From 0ffc4731697da7a475cfe15894c45d53e1e115be Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Tue, 23 Jul 2024 11:49:23 +0200 Subject: [PATCH 01/46] =?UTF-8?q?=E2=9C=A8=20(keyring-eth):=20Implement=20?= =?UTF-8?q?SignTransactionCommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/SignTransactionCommand.test.ts | 467 ++++++++++++++++++ .../command/SignTransactionCommand.ts | 139 +++++- 2 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.test.ts diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.test.ts new file mode 100644 index 000000000..97aef898a --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.test.ts @@ -0,0 +1,467 @@ +import { + ApduResponse, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; +import { SignTransactionCommand } from "./SignTransactionCommand"; +import { Just, Nothing } from "purify-ts"; + +const SIGN_TRANSACTION_APDU_WITHOUT_DATA_FIRST = new Uint8Array([ + 0xe0, 0x04, 0x00, 0x00, 0x15, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x00, + 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]); + +const SIGN_TRANSACTION_APDU_WITHOUT_DATA_SECOND = new Uint8Array([ + 0xe0, 0x04, 0x80, 0x00, 0x00, +]); + +// tx eth 0x8ee3747112cd6cc9c045d6677e65e1c7b39b26bd612b745b8579ddede462f1c4 +const DATA_1_CHUNK = new Uint8Array([ + 0xe9, 0x02, 0x85, 0x01, 0x9e, 0x3c, 0x71, 0xc5, 0x82, 0x52, 0x08, 0x94, 0xd8, + 0xda, 0x6b, 0xf2, 0x69, 0x64, 0xaf, 0x9d, 0x7e, 0xed, 0x9e, 0x03, 0xe5, 0x34, + 0x15, 0xd3, 0x7a, 0xa9, 0x60, 0x45, 0x85, 0x17, 0x48, 0x76, 0xe8, 0x00, 0x80, + 0x01, 0x80, 0x80, +]); + +const SIGN_TRANSACTION_APDU_WITH_1_CHUNK = new Uint8Array([ + ...new Uint8Array([ + 0xe0, 0x04, 0x00, 0x00, 0x3f, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, + 0x00, 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]), + ...DATA_1_CHUNK, +]); + +// tx eth 0xe7950ad373ebad3b63bf1c35c1bf0862c95cd0d0756f152fa7044c547402904b +const DATA_MULTIPLE_CHUNKS = new Uint8Array([ + 0xf9, 0x08, 0xaf, 0x26, 0x85, 0x01, 0xb2, 0x3d, 0x94, 0x83, 0x83, 0x05, 0xc1, + 0xfc, 0x94, 0xde, 0xf1, 0xc0, 0xde, 0xd9, 0xbe, 0xc7, 0xf1, 0xa1, 0x67, 0x08, + 0x19, 0x83, 0x32, 0x40, 0xf0, 0x27, 0xb2, 0x5e, 0xff, 0x80, 0xb9, 0x08, 0x88, + 0x41, 0x55, 0x65, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0x98, 0x40, 0xa8, 0x5d, 0x5a, 0xf5, 0xbf, 0x1d, 0x17, + 0x62, 0xf9, 0x25, 0xbd, 0xad, 0xdc, 0x42, 0x01, 0xf9, 0x84, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xb8, 0x69, 0x91, + 0xc6, 0x21, 0x8b, 0x36, 0xc1, 0xd1, 0x9d, 0x4a, 0x2e, 0x9e, 0xb0, 0xce, 0x36, + 0x06, 0xeb, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x61, 0xe9, 0x33, 0x59, 0x53, 0x95, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x33, + 0xef, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0x98, 0x40, 0xa8, 0x5d, 0x5a, 0xf5, 0xbf, 0x1d, 0x17, 0x62, + 0xf9, 0x25, 0xbd, 0xad, 0xdc, 0x42, 0x01, 0xf9, 0x84, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xb8, 0x69, 0x91, 0xc6, + 0x21, 0x8b, 0x36, 0xc1, 0xd1, 0x9d, 0x4a, 0x2e, 0x9e, 0xb0, 0xce, 0x36, 0x06, + 0xeb, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xe0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0xe9, + 0x33, 0x59, 0x53, 0x95, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x55, 0x6e, 0x69, 0x73, 0x77, 0x61, + 0x70, 0x56, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0xe9, 0x33, 0x59, 0x53, + 0x95, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x3d, 0x0d, 0xae, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x92, 0x42, 0x7a, 0x0a, 0xec, + 0xe9, 0x2d, 0xe3, 0xed, 0xee, 0x1f, 0x18, 0xe0, 0x15, 0x7c, 0x05, 0x86, 0x15, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x1f, 0x98, 0x40, 0xa8, 0x5d, 0x5a, 0xf5, + 0xbf, 0x1d, 0x17, 0x62, 0xf9, 0x25, 0xbd, 0xad, 0xdc, 0x42, 0x01, 0xf9, 0x84, + 0x00, 0x0b, 0xb8, 0xa0, 0xb8, 0x69, 0x91, 0xc6, 0x21, 0x8b, 0x36, 0xc1, 0xd1, + 0x9d, 0x4a, 0x2e, 0x9e, 0xb0, 0xce, 0x36, 0x06, 0xeb, 0x48, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xa0, 0xb8, 0x69, 0x91, 0xc6, 0x21, 0x8b, 0x36, 0xc1, 0xd1, 0x9d, 0x4a, 0x2e, + 0x9e, 0xb0, 0xce, 0x36, 0x06, 0xeb, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x4a, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, + 0x2f, 0xfc, 0xe2, 0x28, 0x72, 0x52, 0xf9, 0x30, 0xe1, 0xc8, 0xdc, 0x93, 0x28, + 0xda, 0xc5, 0xbf, 0x28, 0x2b, 0xa1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xb8, 0x69, 0x91, + 0xc6, 0x21, 0x8b, 0x36, 0xc1, 0xd1, 0x9d, 0x4a, 0x2e, 0x9e, 0xb0, 0xce, 0x36, + 0x06, 0xeb, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4, 0x3c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x01, 0xc2, 0x0d, 0x58, + 0x86, 0x13, 0x7e, 0x05, 0x67, 0x75, 0xaf, 0x56, 0x91, 0x5d, 0xe8, 0x24, 0xc8, + 0xfc, 0xe5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1f, 0x98, 0x40, 0xa8, 0x5d, 0x5a, 0xf5, 0xbf, 0x1d, + 0x17, 0x62, 0xf9, 0x25, 0xbd, 0xad, 0xdc, 0x42, 0x01, 0xf9, 0x84, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xee, 0xee, 0xee, + 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, + 0xee, 0xee, 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x95, 0x84, + 0xcd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x38, 0x2f, 0xfc, 0xe2, 0x28, 0x72, 0x52, 0xf9, 0x30, 0xe1, 0xc8, 0xdc, 0x93, + 0x28, 0xda, 0xc5, 0xbf, 0x28, 0x2b, 0xa1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xf9, 0x14, 0x8f, 0x87, 0x7d, 0xa8, 0x9b, 0xe8, 0x0b, 0xd4, 0x62, 0x23, + 0x01, 0x80, 0x80, +]); + +const SIGN_TRANSACTION_APDU_WITH_MULTIPLE_CHUNKS_FIRST = new Uint8Array([ + ...new Uint8Array([ + 0xe0, 0x04, 0x00, 0x00, 0x96, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, + 0x00, 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]), + // 0 to 129: 130 bytes -> 150 total - 4 * 5 (paths) + ...DATA_MULTIPLE_CHUNKS.slice(0, 129), +]); + +const SIGN_TRANSACTION_APDU_WITH_MULTIPLE_CHUNKS_SECOND = new Uint8Array([ + ...new Uint8Array([0xe0, 0x04, 0x80, 0x00, 0x96]), + ...DATA_MULTIPLE_CHUNKS.slice(129, 279), +]); + +const SIGN_TRANSACTION_APDU_WITH_MULTIPLE_CHUNKS_THIRD = new Uint8Array([ + ...new Uint8Array([0xe0, 0x04, 0x80, 0x00, 0x96]), + ...DATA_MULTIPLE_CHUNKS.slice(279, 279 + 150), +]); + +const SIGN_TRANSACTION_APDU_WITH_MULTIPLE_CHUNKS_LAST = new Uint8Array([ + ...new Uint8Array([0xe0, 0x04, 0x80, 0x00, 0x93]), + ...DATA_MULTIPLE_CHUNKS.slice(2079, 2079 + 147), +]); + +const LNX_RESPONSE_GOOD = new ApduResponse({ + statusCode: Uint8Array.from([0x90, 0x00]), + data: new Uint8Array([]), +}); + +const LNX_RESPONSE_DATA = new Uint8Array([ + 0x26, 0x8d, 0x27, 0x44, 0x47, 0x11, 0xbb, 0xed, 0x44, 0x2b, 0x9b, 0xfc, 0x77, + 0x05, 0xc0, 0x73, 0x16, 0xb7, 0xe4, 0x11, 0x50, 0xc5, 0x33, 0x12, 0x72, 0xe4, + 0xd2, 0x09, 0xd4, 0x22, 0xf9, 0xfa, 0x39, 0x00, 0xcc, 0x3f, 0x0c, 0x19, 0x38, + 0xc0, 0xf1, 0xff, 0xc6, 0x2d, 0xf0, 0x37, 0x22, 0x5a, 0x13, 0x36, 0xfb, 0xa1, + 0xf9, 0xfe, 0xfa, 0x11, 0xf5, 0xaf, 0xc5, 0xbc, 0xb9, 0x7e, 0xb1, 0xb3, 0xd1, + 0x90, 0x00, +]); + +const LNX_RESPONSE_DATA_GOOD = new ApduResponse({ + statusCode: Uint8Array.from([0x90, 0x00]), + data: LNX_RESPONSE_DATA, +}); + +describe("SignTransactionCommand", () => { + const defaultArgs = { + derivationPath: "44'/60'/0'/0/0", + data: new Uint8Array(), + index: 0, + }; + + describe("getApdu", () => { + it("should return the SignTransaction APDU without data when isFirst is true", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + SIGN_TRANSACTION_APDU_WITHOUT_DATA_FIRST, + ); + }); + + it("should return the SignTransaction APDU with data when isFirst is false", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + index: 1, + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + SIGN_TRANSACTION_APDU_WITHOUT_DATA_SECOND, + ); + }); + + it("should return the SignTransaction APDU when data is 1 chunk", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + data: DATA_1_CHUNK, + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + SIGN_TRANSACTION_APDU_WITH_1_CHUNK, + ); + }); + + it("should return the first SignTransaction APDU when data is multiple chunks", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + data: DATA_MULTIPLE_CHUNKS, + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + SIGN_TRANSACTION_APDU_WITH_MULTIPLE_CHUNKS_FIRST, + ); + }); + + it("should return the second SignTransaction APDU when data is multiple chunks", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + data: DATA_MULTIPLE_CHUNKS, + index: 1, + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + SIGN_TRANSACTION_APDU_WITH_MULTIPLE_CHUNKS_SECOND, + ); + }); + + it("should return the third SignTransaction APDU when data is multiple chunks", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + data: DATA_MULTIPLE_CHUNKS, + index: 2, + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + SIGN_TRANSACTION_APDU_WITH_MULTIPLE_CHUNKS_THIRD, + ); + }); + + it("should return the last SignTransaction APDU when data is multiple chunks", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + data: DATA_MULTIPLE_CHUNKS, + index: 14, + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + SIGN_TRANSACTION_APDU_WITH_MULTIPLE_CHUNKS_LAST, + ); + }); + }); + + describe("parseResponse", () => { + it("should return Nothing when the response data is empty", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + }); + + // WHEN + const response = command.parseResponse(LNX_RESPONSE_GOOD); + + // THEN + expect(response).toStrictEqual(Nothing); + }); + + it("should return Just the response data when the response data is not empty", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + }); + + // WHEN + const response = command.parseResponse(LNX_RESPONSE_DATA_GOOD); + + // THEN + expect(response).toStrictEqual( + Just({ + r: "0x8d27444711bbed442b9bfc7705c07316b7e41150c5331272e4d209d422f9fa39", + s: "0x00cc3f0c1938c0f1ffc62df037225a1336fba1f9fefa11f5afc5bcb97eb1b3d1", + v: 38, + }), + ); + }); + + it("should throw an error when the response status code is not 0x9000", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + }); + + // WHEN + const response = () => + command.parseResponse( + new ApduResponse({ + statusCode: Uint8Array.from([0x51, 0x55]), + data: new Uint8Array(), + }), + ); + + // THEN + expect(response).toThrow(InvalidStatusWordError); + }); + + it("should throw an error when the response data r is not valid", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + }); + + // WHEN + const response = () => + command.parseResponse( + new ApduResponse({ + statusCode: Uint8Array.from([0x90, 0x00]), + data: LNX_RESPONSE_DATA.slice(0, 1), + }), + ); + + // THEN + expect(response).toThrow(InvalidStatusWordError); + }); + + it("should throw an error when the response data s is not valid", () => { + // GIVEN + const command = new SignTransactionCommand({ + ...defaultArgs, + }); + + // WHEN + const response = () => + command.parseResponse( + new ApduResponse({ + statusCode: Uint8Array.from([0x90, 0x00]), + data: LNX_RESPONSE_DATA.slice(0, 33), + }), + ); + + // THEN + expect(response).toThrow(InvalidStatusWordError); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts index 041be6c43..9de414a7c 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts @@ -1,2 +1,139 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#sign-eth-transaction -export class SignTransactionCommand {} +import { + Apdu, + ApduBuilder, + ApduBuilderArgs, + ApduParser, + ApduResponse, + type Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; +import { Just } from "purify-ts"; +import { Nothing } from "purify-ts"; +import { Maybe } from "purify-ts"; + +const MAX_CHUNK_SIZE = 150; +const PATH_SIZE = 4; +const R_LENGTH = 32; +const S_LENGTH = 32; + +export type SignTransactionCommandResponse = Maybe<{ + v: number; + r: `0x${string}`; + s: `0x${string}`; +}>; + +export type SignTransactionCommandArgs = { + /** + * The derivation path to use to sign the transaction. + */ + derivationPath: string; + + /** + * The complete serialized transaction data. + */ + data: Uint8Array; + + /** + * The index of the chunk to sign. + */ + index: number; +}; + +export class SignTransactionCommand + implements + Command +{ + args: SignTransactionCommandArgs; + + constructor(args: SignTransactionCommandArgs) { + this.args = args; + } + + getApdu(): Apdu { + const { data, derivationPath, index } = this.args; + + const signEthTransactionArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x04, + p1: index === 0 ? 0x00 : 0x80, + p2: 0x00, + }; + const builder = new ApduBuilder(signEthTransactionArgs); + const path = this.splitPath(derivationPath); + const dataFirstChunkIndex = MAX_CHUNK_SIZE - path.length * PATH_SIZE - 1; + + if (index === 0) { + // add derivation path to the first packet + builder.add8BitUintToData(path.length); + path.forEach((element) => { + builder.add32BitUintToData(element); + }); + + // add 150 bytes of data minus the path length and the path + builder.addBufferToData(data.slice(0, dataFirstChunkIndex)); + } else { + // add 150 bytes of data starting from the second packet + builder.addBufferToData( + data.slice( + dataFirstChunkIndex + (index - 1) * MAX_CHUNK_SIZE, + dataFirstChunkIndex + index * MAX_CHUNK_SIZE, + ), + ); + } + + return builder.build(); + } + + parseResponse(response: ApduResponse): SignTransactionCommandResponse { + const parser = new ApduParser(response); + + // TODO: handle the error correctly using a generic error handler + if (!CommandUtils.isSuccessResponse(response)) { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + response.statusCode, + )}`, + ); + } + + // The data is returned only for the last chunk + const v = parser.extract8BitUint(); + if (!v) { + return Nothing; + } + + const r = parser.encodeToHexaString(parser.extractFieldByLength(R_LENGTH)); + if (!r) { + throw new InvalidStatusWordError("R is missing"); + } + + const s = parser.encodeToHexaString(parser.extractFieldByLength(S_LENGTH)); + if (!s) { + throw new InvalidStatusWordError("S is missing"); + } + + return Just({ + v, + r: `0x${r}`, + s: `0x${s}`, + }); + } + + private splitPath(path: string): number[] { + const result: number[] = []; + const components = path.split("/"); + components.forEach((element) => { + let number = parseInt(element, 10); + if (isNaN(number)) { + return; // FIXME: shouldn't it throws instead? + } + if (element.length > 1 && element[element.length - 1] === "'") { + number += 0x80000000; + } + result.push(number); + }); + return result; + } +} From 3d2abc495d5fde8815c568859cd77c8a226a9159 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Mon, 29 Jul 2024 10:18:43 +0200 Subject: [PATCH 02/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(core):=20Add=20Hexa?= =?UTF-8?q?String=20type=20to=20core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/api/apdu/utils/ApduParser.ts | 12 ++++++++---- packages/core/src/api/index.ts | 1 + packages/core/src/api/types.ts | 1 + .../src/api/utils}/HexString.test.ts | 16 ++++++++-------- packages/core/src/api/utils/HexaString.ts | 5 +++++ packages/signer/context-module/package.json | 4 ++++ .../data/ExternalPluginDataSource.ts | 4 ++-- .../domain/ExternalPluginContextLoader.ts | 8 ++++---- .../src/nft/domain/NftContextLoader.ts | 8 ++++---- .../src/shared/model/HexString.ts | 5 ----- .../src/token/domain/TokenContextLoader.ts | 8 ++++---- .../keyring-eth/src/api/model/Address.ts | 4 +++- .../keyring-eth/src/api/model/Signature.ts | 4 +++- .../app-binder/command/GetAddressCommand.ts | 11 +++++++++-- .../command/SignTransactionCommand.test.ts | 3 ++- .../command/SignTransactionCommand.ts | 19 +++++++++++++------ pnpm-lock.yaml | 3 +++ 17 files changed, 74 insertions(+), 42 deletions(-) rename packages/{signer/context-module/src/shared/model => core/src/api/utils}/HexString.test.ts (73%) create mode 100644 packages/core/src/api/utils/HexaString.ts delete mode 100644 packages/signer/context-module/src/shared/model/HexString.ts diff --git a/packages/core/src/api/apdu/utils/ApduParser.ts b/packages/core/src/api/apdu/utils/ApduParser.ts index b36a9ed3f..80449262f 100644 --- a/packages/core/src/api/apdu/utils/ApduParser.ts +++ b/packages/core/src/api/apdu/utils/ApduParser.ts @@ -1,4 +1,5 @@ import { ApduResponse } from "@api/device-session/ApduResponse"; +import { HexaString } from "@api/utils/HexaString"; export type TaggedField = { readonly tag: number; @@ -127,20 +128,23 @@ export class ApduParser { * @param prefix {boolean} - Whether to add a prefix to the encoded value * @returns {string} - The encoded value as a hexadecimal string */ - encodeToHexaString(value?: Uint8Array, prefix?: boolean): string { + encodeToHexaString(value?: Uint8Array, prefix?: false): string; + encodeToHexaString(value?: Uint8Array, prefix?: true): HexaString; + encodeToHexaString( + value?: Uint8Array, + prefix: boolean = false, + ): HexaString | string { let result = ""; let index = 0; if (!value) return result; - if (prefix) result += "0x"; - while (index <= value.length) { const item = value[index]?.toString(16); if (item) result += item.length < 2 ? "0" + item : item; index++; } - return result; + return prefix ? `0x${result}` : result; } /** diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index b72d1afce..6ee653a01 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -88,3 +88,4 @@ export { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMac export { XStateDeviceAction } from "@api/device-action/xstate-utils/XStateDeviceAction"; export { type DeviceSessionState } from "@api/device-session/DeviceSessionState"; export { type SdkError } from "@api/Error"; +export { isHexaString } from "@api/utils/HexaString"; diff --git a/packages/core/src/api/types.ts b/packages/core/src/api/types.ts index ffe2b0888..6cd0a18aa 100644 --- a/packages/core/src/api/types.ts +++ b/packages/core/src/api/types.ts @@ -9,6 +9,7 @@ export type { SendCommandUseCaseArgs } from "@api/command/use-case/SendCommandUs export type { DeviceModelId } from "@api/device/DeviceModel"; export type { ExecuteDeviceActionUseCaseArgs } from "@api/device-action/use-case/ExecuteDeviceActionUseCase"; export type { DeviceSessionId } from "@api/device-session/types"; +export type { HexaString } from "@api/utils/HexaString"; export type { ConnectUseCaseArgs } from "@internal/discovery/use-case/ConnectUseCase"; export type { DisconnectUseCaseArgs } from "@internal/discovery/use-case/DisconnectUseCase"; export type { SendApduUseCaseArgs } from "@internal/send/use-case/SendApduUseCase"; diff --git a/packages/signer/context-module/src/shared/model/HexString.test.ts b/packages/core/src/api/utils/HexString.test.ts similarity index 73% rename from packages/signer/context-module/src/shared/model/HexString.test.ts rename to packages/core/src/api/utils/HexString.test.ts index 613d82d9b..b979fa12d 100644 --- a/packages/signer/context-module/src/shared/model/HexString.test.ts +++ b/packages/core/src/api/utils/HexString.test.ts @@ -1,13 +1,13 @@ -import { isHexString } from "./HexString"; +import { isHexaString } from "./HexaString"; -describe("HexString", () => { - describe("isHexString function", () => { +describe("HexaString", () => { + describe("isHexaString function", () => { it("should return true if the value is a valid hex string", () => { // GIVEN const value = "0x1234abc"; // WHEN - const result = isHexString(value); + const result = isHexaString(value); // THEN expect(result).toBeTruthy(); @@ -18,7 +18,7 @@ describe("HexString", () => { const value = "0x"; // WHEN - const result = isHexString(value); + const result = isHexaString(value); // THEN expect(result).toBeTruthy(); @@ -29,7 +29,7 @@ describe("HexString", () => { const value = "0x1234z"; // WHEN - const result = isHexString(value); + const result = isHexaString(value); // THEN expect(result).toBeFalsy(); @@ -40,7 +40,7 @@ describe("HexString", () => { const value = "1234abc"; // WHEN - const result = isHexString(value); + const result = isHexaString(value); // THEN expect(result).toBeFalsy(); @@ -51,7 +51,7 @@ describe("HexString", () => { const value = ""; // WHEN - const result = isHexString(value); + const result = isHexaString(value); // THEN expect(result).toBeFalsy(); diff --git a/packages/core/src/api/utils/HexaString.ts b/packages/core/src/api/utils/HexaString.ts new file mode 100644 index 000000000..1533b4194 --- /dev/null +++ b/packages/core/src/api/utils/HexaString.ts @@ -0,0 +1,5 @@ +export type HexaString = `0x${string}`; + +export const isHexaString = (value: string): value is HexaString => { + return /^0x[0-9a-fA-F]*$/.test(value); +}; diff --git a/packages/signer/context-module/package.json b/packages/signer/context-module/package.json index e007ce879..d06eadc28 100644 --- a/packages/signer/context-module/package.json +++ b/packages/signer/context-module/package.json @@ -41,6 +41,7 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { + "@ledgerhq/device-sdk-core": "workspace:*", "@ledgerhq/eslint-config-dsdk": "workspace:*", "@ledgerhq/jest-config-dsdk": "workspace:*", "@ledgerhq/prettier-config-dsdk": "workspace:*", @@ -53,5 +54,8 @@ "inversify": "^6.0.2", "purify-ts": "^2.1.0", "reflect-metadata": "^0.2.2" + }, + "peerDependencies": { + "@ledgerhq/device-sdk-core": "workspace:*" } } diff --git a/packages/signer/context-module/src/external-plugin/data/ExternalPluginDataSource.ts b/packages/signer/context-module/src/external-plugin/data/ExternalPluginDataSource.ts index 694df54c6..328c66fac 100644 --- a/packages/signer/context-module/src/external-plugin/data/ExternalPluginDataSource.ts +++ b/packages/signer/context-module/src/external-plugin/data/ExternalPluginDataSource.ts @@ -1,11 +1,11 @@ +import { HexaString } from "@ledgerhq/device-sdk-core"; import { Either } from "purify-ts"; import { DappInfos } from "@/external-plugin/model/DappInfos"; -import { HexString } from "@/shared/model/HexString"; export type GetDappInfos = { address: string; - selector: HexString; + selector: HexaString; chainId: number; }; diff --git a/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.ts b/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.ts index 7e769c8b8..e7efde1b3 100644 --- a/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.ts +++ b/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.ts @@ -1,3 +1,4 @@ +import { HexaString, isHexaString } from "@ledgerhq/device-sdk-core"; import { ethers } from "ethers"; import { Interface } from "ethers/lib/utils"; import { inject, injectable } from "inversify"; @@ -7,7 +8,6 @@ import type { ExternalPluginDataSource } from "@/external-plugin/data/ExternalPl import { externalPluginTypes } from "@/external-plugin/di/externalPluginTypes"; import { ContextLoader } from "@/shared/domain/ContextLoader"; import { ClearSignContext } from "@/shared/model/ClearSignContext"; -import { HexString, isHexString } from "@/shared/model/HexString"; import { TransactionContext } from "@/shared/model/TransactionContext"; import type { TokenDataSource } from "@/token/data/TokenDataSource"; import { tokenTypes } from "@/token/di/tokenTypes"; @@ -33,7 +33,7 @@ export class ExternalPluginContextLoader implements ContextLoader { const selector = transaction.data.slice(0, 10); - if (!isHexString(selector)) { + if (!isHexaString(selector)) { return [{ type: "error" as const, error: new Error("Invalid selector") }]; } @@ -144,7 +144,7 @@ export class ExternalPluginContextLoader implements ContextLoader { private getAddressFromPath( path: string, decodedCallData: ethers.utils.Result, - ): HexString { + ): HexaString { // ethers.utils.Result is a record string, any // eslint-disable-next-line @typescript-eslint/no-explicit-any let value: any = decodedCallData; @@ -160,7 +160,7 @@ export class ExternalPluginContextLoader implements ContextLoader { } } - if (!isHexString(value)) { + if (!isHexaString(value)) { throw new Error( "[ContextModule] ExternalPluginContextLoader: Unable to get address", ); diff --git a/packages/signer/context-module/src/nft/domain/NftContextLoader.ts b/packages/signer/context-module/src/nft/domain/NftContextLoader.ts index c5e40862f..a8fd3bc44 100644 --- a/packages/signer/context-module/src/nft/domain/NftContextLoader.ts +++ b/packages/signer/context-module/src/nft/domain/NftContextLoader.ts @@ -1,10 +1,10 @@ +import { HexaString, isHexaString } from "@ledgerhq/device-sdk-core"; import { inject, injectable } from "inversify"; import type { NftDataSource } from "@/nft/data/NftDataSource"; import { nftTypes } from "@/nft/di/nftTypes"; import { ContextLoader } from "@/shared/domain/ContextLoader"; import { ClearSignContext } from "@/shared/model/ClearSignContext"; -import { HexString, isHexString } from "@/shared/model/HexString"; import { TransactionContext } from "@/shared/model/TransactionContext"; enum ERC721_SUPPORTED_SELECTOR { @@ -21,7 +21,7 @@ enum ERC1155_SUPPORTED_SELECTOR { SafeBatchTransferFrom = "0x2eb2c2d6", } -const SUPPORTED_SELECTORS: HexString[] = [ +const SUPPORTED_SELECTORS: HexaString[] = [ ...Object.values(ERC721_SUPPORTED_SELECTOR), ...Object.values(ERC1155_SUPPORTED_SELECTOR), ]; @@ -43,7 +43,7 @@ export class NftContextLoader implements ContextLoader { const selector = transaction.data.slice(0, 10); - if (!isHexString(selector)) { + if (!isHexaString(selector)) { return [{ type: "error", error: new Error("Invalid selector") }]; } @@ -98,7 +98,7 @@ export class NftContextLoader implements ContextLoader { return responses; } - private isSelectorSupported(selector: HexString) { + private isSelectorSupported(selector: HexaString) { return Object.values(SUPPORTED_SELECTORS).includes(selector); } } diff --git a/packages/signer/context-module/src/shared/model/HexString.ts b/packages/signer/context-module/src/shared/model/HexString.ts deleted file mode 100644 index 4e1ddad3d..000000000 --- a/packages/signer/context-module/src/shared/model/HexString.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type HexString = `0x${string}`; - -export const isHexString = (value: string): value is HexString => { - return /^0x[0-9a-fA-F]*$/.test(value); -}; diff --git a/packages/signer/context-module/src/token/domain/TokenContextLoader.ts b/packages/signer/context-module/src/token/domain/TokenContextLoader.ts index c267f68ac..9ce08bf4e 100644 --- a/packages/signer/context-module/src/token/domain/TokenContextLoader.ts +++ b/packages/signer/context-module/src/token/domain/TokenContextLoader.ts @@ -1,8 +1,8 @@ +import { HexaString, isHexaString } from "@ledgerhq/device-sdk-core"; import { inject, injectable } from "inversify"; import { ContextLoader } from "@/shared/domain/ContextLoader"; import { ClearSignContext } from "@/shared/model/ClearSignContext"; -import { HexString, isHexString } from "@/shared/model/HexString"; import { TransactionContext } from "@/shared/model/TransactionContext"; import type { TokenDataSource } from "@/token/data/TokenDataSource"; import { tokenTypes } from "@/token/di/tokenTypes"; @@ -12,7 +12,7 @@ export enum ERC20_SUPPORTED_SELECTORS { Transfer = "0xa9059cbb", } -const SUPPORTED_SELECTORS: HexString[] = Object.values( +const SUPPORTED_SELECTORS: HexaString[] = Object.values( ERC20_SUPPORTED_SELECTORS, ); @@ -31,7 +31,7 @@ export class TokenContextLoader implements ContextLoader { const selector = transaction.data.slice(0, 10); - if (!isHexString(selector)) { + if (!isHexaString(selector)) { return [{ type: "error", error: new Error("Invalid selector") }]; } @@ -55,7 +55,7 @@ export class TokenContextLoader implements ContextLoader { ]; } - private isSelectorSupported(selector: HexString) { + private isSelectorSupported(selector: HexaString) { return Object.values(SUPPORTED_SELECTORS).includes(selector); } } diff --git a/packages/signer/keyring-eth/src/api/model/Address.ts b/packages/signer/keyring-eth/src/api/model/Address.ts index adbd893fa..3439d7040 100644 --- a/packages/signer/keyring-eth/src/api/model/Address.ts +++ b/packages/signer/keyring-eth/src/api/model/Address.ts @@ -1,5 +1,7 @@ +import { HexaString } from "@ledgerhq/device-sdk-core"; + export type Address = { - address: `0x${string}`; + address: HexaString; publicKey: string; chainCode?: string; }; diff --git a/packages/signer/keyring-eth/src/api/model/Signature.ts b/packages/signer/keyring-eth/src/api/model/Signature.ts index c7b0656eb..c261c060a 100644 --- a/packages/signer/keyring-eth/src/api/model/Signature.ts +++ b/packages/signer/keyring-eth/src/api/model/Signature.ts @@ -1 +1,3 @@ -export type Signature = { r: `0x${string}`; s: `0x${string}`; v: number }; +import { HexaString } from "@ledgerhq/device-sdk-core"; + +export type Signature = { r: HexaString; s: HexaString; v: number }; diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/GetAddressCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/GetAddressCommand.ts index 866f7cfa9..b8f3caedb 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/GetAddressCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/GetAddressCommand.ts @@ -7,6 +7,7 @@ import { type Command, CommandUtils, InvalidStatusWordError, + isHexaString, } from "@ledgerhq/device-sdk-core"; import { @@ -82,10 +83,16 @@ export class GetAddressCommand throw new InvalidStatusWordError("Ethereum address is missing"); } - const address = parser.encodeToString( + const result = parser.encodeToString( parser.extractFieldByLength(addressLength), ); + const address = `0x${result}`; + + if (isHexaString(address) === false) { + throw new InvalidStatusWordError("Invalid Ethereum address"); + } + let chainCode = undefined; if (this.args.returnChainCode) { if (parser.testMinimalLength(CHAIN_CODE_LENGTH) === false) { @@ -99,7 +106,7 @@ export class GetAddressCommand return { publicKey, - address: `0x${address}`, + address, chainCode, }; } diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.test.ts index 97aef898a..da5480a45 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.test.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.test.ts @@ -2,9 +2,10 @@ import { ApduResponse, InvalidStatusWordError, } from "@ledgerhq/device-sdk-core"; -import { SignTransactionCommand } from "./SignTransactionCommand"; import { Just, Nothing } from "purify-ts"; +import { SignTransactionCommand } from "./SignTransactionCommand"; + const SIGN_TRANSACTION_APDU_WITHOUT_DATA_FIRST = new Uint8Array([ 0xe0, 0x04, 0x00, 0x00, 0x15, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x00, 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts index 9de414a7c..7050ad17d 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts @@ -7,6 +7,7 @@ import { ApduResponse, type Command, CommandUtils, + HexaString, InvalidStatusWordError, } from "@ledgerhq/device-sdk-core"; import { Just } from "purify-ts"; @@ -20,8 +21,8 @@ const S_LENGTH = 32; export type SignTransactionCommandResponse = Maybe<{ v: number; - r: `0x${string}`; - s: `0x${string}`; + r: HexaString; + s: HexaString; }>; export type SignTransactionCommandArgs = { @@ -104,20 +105,26 @@ export class SignTransactionCommand return Nothing; } - const r = parser.encodeToHexaString(parser.extractFieldByLength(R_LENGTH)); + const r = parser.encodeToHexaString( + parser.extractFieldByLength(R_LENGTH), + true, + ); if (!r) { throw new InvalidStatusWordError("R is missing"); } - const s = parser.encodeToHexaString(parser.extractFieldByLength(S_LENGTH)); + const s = parser.encodeToHexaString( + parser.extractFieldByLength(S_LENGTH), + true, + ); if (!s) { throw new InvalidStatusWordError("S is missing"); } return Just({ v, - r: `0x${r}`, - s: `0x${s}`, + r, + s, }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f75fa9a9..c931010ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,6 +233,9 @@ importers: specifier: ^0.2.2 version: 0.2.2 devDependencies: + '@ledgerhq/device-sdk-core': + specifier: workspace:* + version: link:../../core '@ledgerhq/eslint-config-dsdk': specifier: workspace:* version: link:../../config/eslint From fa1796c13a3173d3f1991dd60ad62b0a0e175ec0 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 24 Jul 2024 12:49:25 +0200 Subject: [PATCH 03/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(keyring-eth):=20Cre?= =?UTF-8?q?ate=20DerivationPathUtils=20to=20split=20derivation=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app-binder/command/GetAddressCommand.ts | 19 +---- .../command/SignTransactionCommand.ts | 20 +---- .../shared/utils/DerivationPathUtils.test.ts | 76 +++++++++++++++++++ .../shared/utils/DerivationPathUtils.ts | 19 +++++ 4 files changed, 100 insertions(+), 34 deletions(-) create mode 100644 packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.test.ts create mode 100644 packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.ts diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/GetAddressCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/GetAddressCommand.ts index b8f3caedb..318b3cfc4 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/GetAddressCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/GetAddressCommand.ts @@ -14,6 +14,7 @@ import { GetAddressCommandArgs, GetAddressCommandResponse, } from "@api/app-binder/GetAddressCommandTypes"; +import { DerivationPathUtils } from "@internal/shared/utils/DerivationPathUtils"; const CHAIN_CODE_LENGTH = 32; @@ -36,7 +37,7 @@ export class GetAddressCommand const builder = new ApduBuilder(getEthAddressArgs); const derivationPath = this.args.derivationPath; - const path = this.splitPath(derivationPath); + const path = DerivationPathUtils.splitPath(derivationPath); builder.add8BitUIntToData(path.length); path.forEach((element) => { builder.add32BitUIntToData(element); @@ -110,20 +111,4 @@ export class GetAddressCommand chainCode, }; } - - private splitPath(path: string): number[] { - const result: number[] = []; - const components = path.split("/"); - components.forEach((element) => { - let number = parseInt(element, 10); - if (isNaN(number)) { - return; // FIXME: shouldn't it throws instead? - } - if (element.length > 1 && element[element.length - 1] === "'") { - number += 0x80000000; - } - result.push(number); - }); - return result; - } } diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts index 7050ad17d..b594b77df 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts @@ -14,6 +14,8 @@ import { Just } from "purify-ts"; import { Nothing } from "purify-ts"; import { Maybe } from "purify-ts"; +import { DerivationPathUtils } from "@internal/shared/utils/DerivationPathUtils"; + const MAX_CHUNK_SIZE = 150; const PATH_SIZE = 4; const R_LENGTH = 32; @@ -62,7 +64,7 @@ export class SignTransactionCommand p2: 0x00, }; const builder = new ApduBuilder(signEthTransactionArgs); - const path = this.splitPath(derivationPath); + const path = DerivationPathUtils.splitPath(derivationPath); const dataFirstChunkIndex = MAX_CHUNK_SIZE - path.length * PATH_SIZE - 1; if (index === 0) { @@ -127,20 +129,4 @@ export class SignTransactionCommand s, }); } - - private splitPath(path: string): number[] { - const result: number[] = []; - const components = path.split("/"); - components.forEach((element) => { - let number = parseInt(element, 10); - if (isNaN(number)) { - return; // FIXME: shouldn't it throws instead? - } - if (element.length > 1 && element[element.length - 1] === "'") { - number += 0x80000000; - } - result.push(number); - }); - return result; - } } diff --git a/packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.test.ts b/packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.test.ts new file mode 100644 index 000000000..2e55de37d --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.test.ts @@ -0,0 +1,76 @@ +import { DerivationPathUtils } from "./DerivationPathUtils"; + +describe("DerivationPathUtils", () => { + it("padding should be 0x80000000", () => { + // GIVEN + const padding = 0x80000000; + + // WHEN + const result = DerivationPathUtils.padding; + + // THEN + expect(result).toBe(padding); + }); + + it("should split the derivation path", () => { + // GIVEN + const path = "44'/60/0/0/0"; + + // WHEN + const result = DerivationPathUtils.splitPath(path); + + // THEN + expect(result).toStrictEqual([ + 44 + DerivationPathUtils.padding, + 60, + 0, + 0, + 0, + ]); + }); + + it("should split the derivation path with hardened path", () => { + // GIVEN + const path = "44'/60'/0'/0'/1"; + + // WHEN + const result = DerivationPathUtils.splitPath(path); + + // THEN + expect(result).toStrictEqual([ + 44 + DerivationPathUtils.padding, + 60 + DerivationPathUtils.padding, + 0 + DerivationPathUtils.padding, + 0 + DerivationPathUtils.padding, + 1, + ]); + }); + + it("should split the derivation path with custom path", () => { + // GIVEN + const path = "44'/60'/5/4/3"; + + // WHEN + const result = DerivationPathUtils.splitPath(path); + + // THEN + expect(result).toStrictEqual([ + 44 + DerivationPathUtils.padding, + 60 + DerivationPathUtils.padding, + 5, + 4, + 3, + ]); + }); + + it("should throw an error if invalid number provided", () => { + // GIVEN + const path = "44'/60'/zzz/4/3"; + + // WHEN + const result = () => DerivationPathUtils.splitPath(path); + + // THEN + expect(result).toThrow(new Error("invalid number provided")); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.ts b/packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.ts new file mode 100644 index 000000000..d9d14bd92 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.ts @@ -0,0 +1,19 @@ +export class DerivationPathUtils { + static splitPath(path: string): number[] { + const result: number[] = []; + const components = path.split("/"); + components.forEach((element) => { + let number = parseInt(element, 10); + if (isNaN(number)) { + throw new Error("invalid number provided"); + } + if (element.length > 1 && element[element.length - 1] === "'") { + number += this.padding; + } + result.push(number); + }); + return result; + } + + static padding = 0x80000000; +} From 861f9c56b7b10034df156e369400dfd614b545f1 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Tue, 23 Jul 2024 12:07:23 +0200 Subject: [PATCH 04/46] =?UTF-8?q?=F0=9F=94=96=20(chore):=20Changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/witty-dancers-boil.md | 5 +++++ .changeset/witty-plums-search.md | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/witty-dancers-boil.md create mode 100644 .changeset/witty-plums-search.md diff --git a/.changeset/witty-dancers-boil.md b/.changeset/witty-dancers-boil.md new file mode 100644 index 000000000..8f9656186 --- /dev/null +++ b/.changeset/witty-dancers-boil.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement SignTransactionCommand diff --git a/.changeset/witty-plums-search.md b/.changeset/witty-plums-search.md new file mode 100644 index 000000000..28176e306 --- /dev/null +++ b/.changeset/witty-plums-search.md @@ -0,0 +1,7 @@ +--- +"@ledgerhq/context-module": patch +"@ledgerhq/keyring-eth": patch +"@ledgerhq/device-sdk-core": patch +--- + +add HexaString to handle `0x${string}` type From 8104b613c0b6aae15bc22ea82e87bb007f5ade5d Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 24 Jul 2024 09:39:32 +0200 Subject: [PATCH 05/46] =?UTF-8?q?=F0=9F=90=9B=20(signer):=20Import=20refle?= =?UTF-8?q?ct-metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/signer/context-module/index.ts | 3 +++ packages/signer/keyring-eth/src/index.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/signer/context-module/index.ts b/packages/signer/context-module/index.ts index e910bb060..f843c430e 100644 --- a/packages/signer/context-module/index.ts +++ b/packages/signer/context-module/index.ts @@ -1 +1,4 @@ +// inversify requirement +import "reflect-metadata"; + export * from "./src/index"; diff --git a/packages/signer/keyring-eth/src/index.ts b/packages/signer/keyring-eth/src/index.ts index 4a2ede760..d1f800da1 100644 --- a/packages/signer/keyring-eth/src/index.ts +++ b/packages/signer/keyring-eth/src/index.ts @@ -1 +1,4 @@ +// inversify requirement +import "reflect-metadata"; + export * from "@api/index"; From c5bf67eaf5698d0c1f281c382b94476fcf617596 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Tue, 23 Jul 2024 17:31:55 +0200 Subject: [PATCH 06/46] =?UTF-8?q?=F0=9F=9A=A8=20(chore):=20Fix=20build=20w?= =?UTF-8?q?ith=20commonjs=20when=20imported=20from=20other?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/tsconfig.cjs.json | 4 ++-- packages/signer/context-module/tsconfig.cjs.json | 4 ++-- packages/signer/keyring-eth/tsconfig.cjs.json | 4 ++-- packages/trusted-apps/tsconfig.cjs.json | 4 ++-- packages/ui/tsconfig.cjs.json | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/tsconfig.cjs.json b/packages/core/tsconfig.cjs.json index 2a786393f..63c80c465 100644 --- a/packages/core/tsconfig.cjs.json +++ b/packages/core/tsconfig.cjs.json @@ -8,8 +8,8 @@ "jest.*.ts" ], "compilerOptions": { - "module": "commonjs", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "outDir": "./lib/cjs", "declarationDir": "./lib/cjs" } diff --git a/packages/signer/context-module/tsconfig.cjs.json b/packages/signer/context-module/tsconfig.cjs.json index a797cc717..3797ab02f 100644 --- a/packages/signer/context-module/tsconfig.cjs.json +++ b/packages/signer/context-module/tsconfig.cjs.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "exclude": ["src/**/*.test.ts", "jest.*.ts"], "compilerOptions": { - "module": "commonjs", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "outDir": "./lib/cjs", "declarationDir": "./lib/cjs" } diff --git a/packages/signer/keyring-eth/tsconfig.cjs.json b/packages/signer/keyring-eth/tsconfig.cjs.json index a797cc717..3797ab02f 100644 --- a/packages/signer/keyring-eth/tsconfig.cjs.json +++ b/packages/signer/keyring-eth/tsconfig.cjs.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "exclude": ["src/**/*.test.ts", "jest.*.ts"], "compilerOptions": { - "module": "commonjs", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "outDir": "./lib/cjs", "declarationDir": "./lib/cjs" } diff --git a/packages/trusted-apps/tsconfig.cjs.json b/packages/trusted-apps/tsconfig.cjs.json index a797cc717..3797ab02f 100644 --- a/packages/trusted-apps/tsconfig.cjs.json +++ b/packages/trusted-apps/tsconfig.cjs.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "exclude": ["src/**/*.test.ts", "jest.*.ts"], "compilerOptions": { - "module": "commonjs", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "outDir": "./lib/cjs", "declarationDir": "./lib/cjs" } diff --git a/packages/ui/tsconfig.cjs.json b/packages/ui/tsconfig.cjs.json index a797cc717..3797ab02f 100644 --- a/packages/ui/tsconfig.cjs.json +++ b/packages/ui/tsconfig.cjs.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "exclude": ["src/**/*.test.ts", "jest.*.ts"], "compilerOptions": { - "module": "commonjs", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "outDir": "./lib/cjs", "declarationDir": "./lib/cjs" } From 3614fa561fd105a8aae53a3ebfaf3d77ab337837 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Mon, 29 Jul 2024 10:00:38 +0200 Subject: [PATCH 07/46] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20(keyring-eth):=20Fix?= =?UTF-8?q?=20uint=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/app-binder/command/SignTransactionCommand.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts index b594b77df..c0113352b 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts @@ -69,9 +69,9 @@ export class SignTransactionCommand if (index === 0) { // add derivation path to the first packet - builder.add8BitUintToData(path.length); + builder.add8BitUIntToData(path.length); path.forEach((element) => { - builder.add32BitUintToData(element); + builder.add32BitUIntToData(element); }); // add 150 bytes of data minus the path length and the path @@ -102,7 +102,7 @@ export class SignTransactionCommand } // The data is returned only for the last chunk - const v = parser.extract8BitUint(); + const v = parser.extract8BitUInt(); if (!v) { return Nothing; } From fb99085aef76cb7b1fcfcc61cafb1eff8d78b524 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 24 Jul 2024 16:15:08 +0200 Subject: [PATCH 08/46] =?UTF-8?q?=E2=9C=A8=20(keyring-eth):=20Add=20serial?= =?UTF-8?q?ized=20transaction=20in=20mapper=20tx=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serialized transaction will be used by the signTransaction DA, subset by the context-module --- .../mapper/EthersV5TransactionMapper.test.ts | 36 +++++++++++---- .../mapper/EthersV5TransactionMapper.ts | 41 +++++++++++------ .../mapper/EthersV6TransactionMapper.test.ts | 45 +++++++++++++------ .../mapper/EthersV6TransactionMapper.ts | 16 ++++--- .../service/mapper/TransactionMapper.ts | 5 ++- .../mapper/TransactionMapperService.ts | 4 +- .../mapper/model/TransactionMapperResult.ts | 13 ++++++ 7 files changed, 113 insertions(+), 47 deletions(-) create mode 100644 packages/signer/keyring-eth/src/internal/transaction/service/mapper/model/TransactionMapperResult.ts diff --git a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV5TransactionMapper.test.ts b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV5TransactionMapper.test.ts index 38c812352..5a2fe6eaa 100644 --- a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV5TransactionMapper.test.ts +++ b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV5TransactionMapper.test.ts @@ -26,6 +26,9 @@ describe("EthersV5TransactionMapper", () => { value: EthersV5BigNumber.from(0), data: "0x", }; + const serialized = new Uint8Array([ + 0xc9, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01, 0x80, 0x80, + ]); // WHEN const result = mapper.map(transaction); @@ -33,9 +36,12 @@ describe("EthersV5TransactionMapper", () => { // THEN expect(result).toEqual( Just({ - chainId: 1, - to: undefined, - data: "0x", + subset: { + chainId: 1, + to: undefined, + data: "0x", + }, + serialized, }), ); }); @@ -50,6 +56,9 @@ describe("EthersV5TransactionMapper", () => { data: "0x", to: "0x", }; + const serialized = new Uint8Array([ + 0xc9, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01, 0x80, 0x80, + ]); // WHEN const result = mapper.map(transaction); @@ -57,9 +66,12 @@ describe("EthersV5TransactionMapper", () => { // THEN expect(result).toEqual( Just({ - chainId: 1, - to: "0x", - data: "0x", + subset: { + chainId: 1, + to: "0x", + data: "0x", + }, + serialized, }), ); }); @@ -81,6 +93,9 @@ describe("EthersV5TransactionMapper", () => { maxFeePerGas: EthersV5BigNumber.from(0), maxPriorityFeePerGas: EthersV5BigNumber.from(0), }; + const serialized = new Uint8Array([ + 0xc9, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01, 0x80, 0x80, + ]); // WHEN const result = mapper.map(transaction); @@ -88,9 +103,12 @@ describe("EthersV5TransactionMapper", () => { // THEN expect(result).toEqual( Just({ - chainId: 1, - to: undefined, - data: "0x", + subset: { + chainId: 1, + to: undefined, + data: "0x", + }, + serialized, }), ); }); diff --git a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV5TransactionMapper.ts b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV5TransactionMapper.ts index 93774cefe..29e2d00fd 100644 --- a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV5TransactionMapper.ts +++ b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV5TransactionMapper.ts @@ -1,22 +1,43 @@ -import { TransactionSubset } from "@ledgerhq/context-module"; -import { BigNumber, Transaction as EthersV5Transaction } from "ethers-v5"; +import { + BigNumber, + ethers, + Transaction as EthersV5Transaction, +} from "ethers-v5"; import { injectable } from "inversify"; import { Just, Maybe, Nothing } from "purify-ts"; import { Transaction } from "@api/index"; +import { TransactionMapperResult } from "./model/TransactionMapperResult"; import { TransactionMapper } from "./TransactionMapper"; @injectable() export class EthersV5TransactionMapper implements TransactionMapper { constructor() {} - map(transaction: Transaction): Maybe { + map(transaction: Transaction): Maybe { if (this.isEthersV5Transaction(transaction)) { - return Just({ - chainId: transaction.chainId, + // ensure that we have a valid non signed transaction + const txUnsigned = { to: transaction.to, + nonce: transaction.nonce, + gasLimit: transaction.gasLimit, + gasPrice: transaction.gasPrice, data: transaction.data, + value: transaction.value, + chainId: transaction.chainId, + }; + const serialized = ethers.utils.arrayify( + ethers.utils.serializeTransaction(txUnsigned), + ); + + return Just({ + subset: { + chainId: transaction.chainId, + to: transaction.to, + data: transaction.data, + }, + serialized, }); } @@ -31,20 +52,12 @@ export class EthersV5TransactionMapper implements TransactionMapper { typeof tx === "object" && tx !== null && (tx.to === undefined || typeof tx.to === "string") && - (tx.from === undefined || typeof tx.from === "string") && typeof tx.nonce === "number" && tx.gasLimit instanceof BigNumber && (tx.gasPrice === undefined || tx.gasPrice instanceof BigNumber) && typeof tx.data === "string" && tx.value instanceof BigNumber && - typeof tx.chainId === "number" && - (tx.r === undefined || typeof tx.r === "string") && - (tx.s === undefined || typeof tx.s === "string") && - (tx.v === undefined || typeof tx.v === "number") && - (tx.type === undefined || typeof tx.type === "number") && - (tx.maxFeePerGas === undefined || tx.maxFeePerGas instanceof BigNumber) && - (tx.maxPriorityFeePerGas === undefined || - tx.maxPriorityFeePerGas instanceof BigNumber) + typeof tx.chainId === "number" ); } } diff --git a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV6TransactionMapper.test.ts b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV6TransactionMapper.test.ts index f94185f7e..203260112 100644 --- a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV6TransactionMapper.test.ts +++ b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV6TransactionMapper.test.ts @@ -19,6 +19,9 @@ describe("EthersV6TransactionMapper", () => { transaction.chainId = 1n; transaction.nonce = 0; transaction.data = "0x"; + const serialized = new Uint8Array([ + 2, 201, 1, 128, 128, 128, 128, 128, 128, 128, 192, + ]); // WHEN const result = mapper.map(transaction); @@ -26,9 +29,12 @@ describe("EthersV6TransactionMapper", () => { // THEN expect(result).toEqual( Just({ - chainId: 1, - to: undefined, - data: "0x", + subset: { + chainId: 1, + to: undefined, + data: "0x", + }, + serialized, }), ); }); @@ -40,6 +46,11 @@ describe("EthersV6TransactionMapper", () => { transaction.nonce = 0; transaction.data = "0x"; transaction.to = "0x0123456789abcdef0123456789abcdef01234567"; + const serialized = new Uint8Array([ + 0x02, 0xdd, 0x01, 0x80, 0x80, 0x80, 0x80, 0x94, 0x01, 0x23, 0x45, 0x67, + 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x80, 0x80, 0xc0, + ]); // WHEN const result = mapper.map(transaction); @@ -47,9 +58,12 @@ describe("EthersV6TransactionMapper", () => { // THEN expect(result).toEqual( Just({ - chainId: 1, - to: "0x0123456789abcDEF0123456789abCDef01234567", - data: "0x", + subset: { + chainId: 1, + to: "0x0123456789abcDEF0123456789abCDef01234567", + data: "0x", + }, + serialized, }), ); }); @@ -63,13 +77,13 @@ describe("EthersV6TransactionMapper", () => { transaction.nonce = 0; transaction.gasLimit = 0n; transaction.gasPrice = 0n; - transaction.maxPriorityFeePerGas = 0n; - transaction.maxFeePerGas = 0n; transaction.value = 0n; transaction.chainId = 1n; - transaction.accessList = []; - transaction.maxFeePerBlobGas = 0n; - transaction.blobs = []; + const serialized = new Uint8Array([ + 0xdd, 0x80, 0x80, 0x80, 0x94, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, + 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, + 0x67, 0x80, 0x80, 0x01, 0x80, 0x80, + ]); // WHEN const result = mapper.map(transaction); @@ -77,9 +91,12 @@ describe("EthersV6TransactionMapper", () => { // THEN expect(result).toEqual( Just({ - chainId: 1, - to: "0x0123456789abcDEF0123456789abCDef01234567", - data: "0x", + subset: { + chainId: 1, + to: "0x0123456789abcDEF0123456789abCDef01234567", + data: "0x", + }, + serialized, }), ); }); diff --git a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV6TransactionMapper.ts b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV6TransactionMapper.ts index d27a11fdb..4745a9307 100644 --- a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV6TransactionMapper.ts +++ b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/EthersV6TransactionMapper.ts @@ -1,20 +1,24 @@ -import { TransactionSubset } from "@ledgerhq/context-module"; -import { Transaction as EthersV6Transaction } from "ethers-v6"; +import { getBytes, Transaction as EthersV6Transaction } from "ethers-v6"; import { injectable } from "inversify"; import { Just, Maybe, Nothing } from "purify-ts"; import { Transaction } from "@api/index"; +import { TransactionMapperResult } from "./model/TransactionMapperResult"; import { TransactionMapper } from "./TransactionMapper"; @injectable() export class EthersV6TransactionMapper implements TransactionMapper { - map(transaction: Transaction): Maybe { + map(transaction: Transaction): Maybe { if (this.isEthersV6Transaction(transaction)) { + const serialized = getBytes(transaction.unsignedSerialized); return Just({ - chainId: Number(transaction.chainId.toString()), - to: transaction.to ?? undefined, - data: transaction.data, + subset: { + chainId: Number(transaction.chainId.toString()), + to: transaction.to ?? undefined, + data: transaction.data, + }, + serialized, }); } diff --git a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/TransactionMapper.ts b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/TransactionMapper.ts index 9a2ca5493..d14a90243 100644 --- a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/TransactionMapper.ts +++ b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/TransactionMapper.ts @@ -1,8 +1,9 @@ -import { TransactionSubset } from "@ledgerhq/context-module"; import { Maybe } from "purify-ts"; import { Transaction } from "@api/index"; +import { TransactionMapperResult } from "./model/TransactionMapperResult"; + export interface TransactionMapper { - map(transaction: Transaction): Maybe; + map(transaction: Transaction): Maybe; } diff --git a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/TransactionMapperService.ts b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/TransactionMapperService.ts index 391a2a661..9a1b8f39a 100644 --- a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/TransactionMapperService.ts +++ b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/TransactionMapperService.ts @@ -1,10 +1,10 @@ -import { TransactionSubset } from "@ledgerhq/context-module"; import { injectable, multiInject } from "inversify"; import { Either, Left, Right } from "purify-ts"; import { Transaction } from "@api/index"; import { transactionTypes } from "@internal/transaction/di/transactionTypes"; +import { TransactionMapperResult } from "./model/TransactionMapperResult"; import { TransactionMapper } from "./TransactionMapper"; @injectable() @@ -20,7 +20,7 @@ export class TransactionMapperService { mapTransactionToSubset( transaction: Transaction, - ): Either { + ): Either { for (const mapper of this._mappers) { const result = mapper.map(transaction); if (result.isJust()) { diff --git a/packages/signer/keyring-eth/src/internal/transaction/service/mapper/model/TransactionMapperResult.ts b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/model/TransactionMapperResult.ts new file mode 100644 index 000000000..832d67fe1 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/transaction/service/mapper/model/TransactionMapperResult.ts @@ -0,0 +1,13 @@ +import { TransactionSubset } from "@ledgerhq/context-module"; + +export type TransactionMapperResult = { + /** + * transaction attributes used for clear signing + */ + subset: TransactionSubset; + + /** + * serialized transaction in Uint8Array format + */ + serialized: Uint8Array; +}; From 463132c253fa7b55f6d7dd5bf3d2ed0e78866bd0 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 24 Jul 2024 16:17:59 +0200 Subject: [PATCH 09/46] =?UTF-8?q?=F0=9F=94=96=20(chore):=20Changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/gentle-zebras-raise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gentle-zebras-raise.md diff --git a/.changeset/gentle-zebras-raise.md b/.changeset/gentle-zebras-raise.md new file mode 100644 index 000000000..a01029723 --- /dev/null +++ b/.changeset/gentle-zebras-raise.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Add transaction serialized to transaction mapper result From 45f5e51857b8857026328fdae83bed8312872872 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:51:27 +0000 Subject: [PATCH 10/46] :arrow_up: (repo) [NO-ISSUE]: Bump @sentry/nextjs from 8.13.0 to 8.20.0 Bumps [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) from 8.13.0 to 8.20.0. - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/8.13.0...8.20.0) --- updated-dependencies: - dependency-name: "@sentry/nextjs" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- apps/sample/package.json | 2 +- pnpm-lock.yaml | 486 +++++++++++++++------------------------ 2 files changed, 186 insertions(+), 302 deletions(-) diff --git a/apps/sample/package.json b/apps/sample/package.json index 63072f80f..cf537ae1c 100644 --- a/apps/sample/package.json +++ b/apps/sample/package.json @@ -16,7 +16,7 @@ "@ledgerhq/device-sdk-core": "workspace:*", "@ledgerhq/keyring-eth": "workspace:*", "@ledgerhq/react-ui": "^0.15.1", - "@sentry/nextjs": "^8.13.0", + "@sentry/nextjs": "^8.20.0", "next": "14.2.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c931010ba..8c522a072 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: ^0.15.1 version: 0.15.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)) '@sentry/nextjs': - specifier: ^8.13.0 - version: 8.13.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1) + specifier: ^8.20.0 + version: 8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1) next: specifier: 14.2.4 version: 14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1774,32 +1774,32 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/instrumentation-connect@0.37.0': - resolution: {integrity: sha512-SeQktDIH5rNzjiEiazWiJAIXkmnLOnNV7wwHpahrqE0Ph+Z3heqMfxRtoMtbdJSIYLfcNZYO51AjxZ00IXufdw==} + '@opentelemetry/instrumentation-connect@0.38.0': + resolution: {integrity: sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-express@0.40.1': - resolution: {integrity: sha512-+RKMvVe2zw3kIXRup9c1jFu3T4d0fs5aKy015TpiMyoCKX1UMu3Z0lfgYtuyiSTANvg5hZnDbWmQmqSPj9VTvg==} + '@opentelemetry/instrumentation-express@0.41.0': + resolution: {integrity: sha512-/B7fbMdaf3SYe5f1P973tkqd6s7XZirjpfkoJ63E7nltU30qmlgm9tY5XwZOzAFI0rHS9tbrFI2HFPAvQUFe/A==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-fastify@0.37.0': - resolution: {integrity: sha512-WRjwzNZgupSzbEYvo9s+QuHJRqZJjVdNxSEpGBwWK8RKLlHGwGVAu0gcc2gPamJWUJsGqPGvahAPWM18ZkWj6A==} + '@opentelemetry/instrumentation-fastify@0.38.0': + resolution: {integrity: sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-graphql@0.41.0': - resolution: {integrity: sha512-R/gXeljgIhaRDKquVkKYT5QHPnFouM8ooyePZEP0kqyaVAedtR1V7NfAUJbxfTG5fBQa5wdmLjvu63+tzRXZCA==} + '@opentelemetry/instrumentation-graphql@0.42.0': + resolution: {integrity: sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-hapi@0.39.0': - resolution: {integrity: sha512-ik2nA9Yj2s2ay+aNY+tJsKCsEx6Tsc2g/MK0iWBW5tibwrWKTy1pdVt5sB3kd5Gkimqj23UV5+FH2JFcQLeKug==} + '@opentelemetry/instrumentation-hapi@0.40.0': + resolution: {integrity: sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -1810,62 +1810,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.41.0': - resolution: {integrity: sha512-rxiLloU8VyeJGm5j2fZS8ShVdB82n7VNP8wTwfUQqDwRfHCnkzGr+buKoxuhGD91gtwJ91RHkjHA1Eg6RqsUTg==} + '@opentelemetry/instrumentation-ioredis@0.42.0': + resolution: {integrity: sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-koa@0.41.0': - resolution: {integrity: sha512-mbPnDt7ELvpM2S0vixYUsde7122lgegLOJQxx8iJQbB8YHal/xnTh9v7IfArSVzIDo+E+080hxZyUZD4boOWkw==} + '@opentelemetry/instrumentation-koa@0.42.0': + resolution: {integrity: sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongodb@0.45.0': - resolution: {integrity: sha512-xnZP9+ayeB1JJyNE9cIiwhOJTzNEsRhXVdLgfzmrs48Chhhk026mQdM5CITfyXSCfN73FGAIB8d91+pflJEfWQ==} + '@opentelemetry/instrumentation-mongodb@0.46.0': + resolution: {integrity: sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongoose@0.39.0': - resolution: {integrity: sha512-J1r66A7zJklPPhMtrFOO7/Ud2p0Pv5u8+r23Cd1JUH6fYPmftNJVsLp2urAt6PHK4jVqpP/YegN8wzjJ2mZNPQ==} + '@opentelemetry/instrumentation-mongoose@0.40.0': + resolution: {integrity: sha512-niRi5ZUnkgzRhIGMOozTyoZIvJKNJyhijQI4nF4iFSb+FUx2v5fngfR+8XLmdQAO7xmsD8E5vEGdDVYVtKbZew==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql2@0.39.0': - resolution: {integrity: sha512-Iypuq2z6TCfriAXCIZjRq8GTFCKhQv5SpXbmI+e60rYdXw8NHtMH4NXcGF0eKTuoCsC59IYSTUvDQYDKReaszA==} + '@opentelemetry/instrumentation-mysql2@0.40.0': + resolution: {integrity: sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql@0.39.0': - resolution: {integrity: sha512-8snHPh83rhrDf31v9Kq0Nf+ts8hdr7NguuszRqZomZBHgE0+UyXZSkXHAAFZoBPPRMGyM68uaFE5hVtFl+wOcA==} + '@opentelemetry/instrumentation-mysql@0.40.0': + resolution: {integrity: sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.38.0': - resolution: {integrity: sha512-M381Df1dM8aqihZz2yK+ugvMFK5vlHG/835dc67Sx2hH4pQEQYDA2PpFPTgc9AYYOydQaj7ClFQunESimjXDgg==} + '@opentelemetry/instrumentation-nestjs-core@0.39.0': + resolution: {integrity: sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.42.0': - resolution: {integrity: sha512-sjgcM8CswYy8zxHgXv4RAZ09DlYhQ+9TdlourUs63Df/ek5RrB1ZbjznqW7PB6c3TyJJmX6AVtPTjAsROovEjA==} + '@opentelemetry/instrumentation-pg@0.43.0': + resolution: {integrity: sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-redis-4@0.40.0': - resolution: {integrity: sha512-0ieQYJb6yl35kXA75LQUPhHtGjtQU9L85KlWa7d4ohBbk/iQKZ3X3CFl5jC5vNMq/GGPB3+w3IxNvALlHtrp7A==} + '@opentelemetry/instrumentation-redis-4@0.41.0': + resolution: {integrity: sha512-H7IfGTqW2reLXqput4yzAe8YpDC0fmVNal95GHMLOrS89W+qWUKIqxolSh63hJyfmwPSFwXASzj7wpSk8Az+Dg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.43.0': - resolution: {integrity: sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ==} + '@opentelemetry/instrumentation@0.46.0': + resolution: {integrity: sha512-a9TijXZZbk0vI5TGLZl+0kxyFfrXHhX6Svtz7Pp2/VBlCSKrazuULEyoJQrOknJyFWNMEmbbJgOciHCCpQcisw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -1931,8 +1931,8 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@prisma/instrumentation@5.16.0': - resolution: {integrity: sha512-MVzNRW2ikWvVNnMIEgQMcwWxpFD+XF2U2h0Qz7MjutRqJxrhWexWV2aSi2OXRaU8UL5wzWw7pnjdKUzYhWauLg==} + '@prisma/instrumentation@5.17.0': + resolution: {integrity: sha512-c1Sle4ji8aasMcYfBBHFM56We4ljfenVtRmS8aY06BllS7SoU6SmJBwG7vil+GHiR0Yrh+t9iBwt4AY0Jr4KNQ==} '@react-native-community/cli-clean@13.6.9': resolution: {integrity: sha512-7Dj5+4p9JggxuVNOjPbduZBAP1SUgNhLKVw5noBUzT/3ZpUZkDM+RCSwyoyg8xKWoE4OrdUAXwAFlMcFDPKykA==} @@ -2054,28 +2054,28 @@ packages: rollup: optional: true - '@sentry-internal/browser-utils@8.13.0': - resolution: {integrity: sha512-lqq8BYbbs9KTlDuyB5NjdZB6P/llqQs32KUgaCQ/k5DFB4Zf56+BFHXObnMHxwx375X1uixtnEphagWZa+nsLQ==} + '@sentry-internal/browser-utils@8.20.0': + resolution: {integrity: sha512-GGYNiELnT4ByidHyS4/M8UF8Oxagm5R13QyTncQGq8nZcQhcFZ9mdxLnf1/R4+j44Fph2Cgzafe8jGP/AMA9zw==} engines: {node: '>=14.18'} - '@sentry-internal/feedback@8.13.0': - resolution: {integrity: sha512-YyJ6SzpTonixvguAg0H9vkEp7Jq8ZeVY8M4n47ClR0+TtaAUp04ZhcJpHKF7PwBIAzc7DRr2XP112tmWgiVEcg==} + '@sentry-internal/feedback@8.20.0': + resolution: {integrity: sha512-mFvAoVpVShkDB2AgEr/dE96NSTPKI/lGMBznZMg7ZEcwZhLfH7HvLYCadIskRfzqFTLOUpbm9ciIO4SyR/4bDA==} engines: {node: '>=14.18'} - '@sentry-internal/replay-canvas@8.13.0': - resolution: {integrity: sha512-lPlfWVIHX+gW4S8a/UOVutuqMyQhlkNUAay0W21MVhZJT5Mtj0p21D/Cz7nrOQRDIiLNq90KAGK2tLxx5NkiWA==} + '@sentry-internal/replay-canvas@8.20.0': + resolution: {integrity: sha512-LXV/pMH9KMw6CtImenMsiBkYIFIc97pDJ/rC7mVImKIROQ45fxGp/JBXM4Id0GENyA2+SySMWVQCAAapSfHZTw==} engines: {node: '>=14.18'} - '@sentry-internal/replay@8.13.0': - resolution: {integrity: sha512-DJ1jF/Pab0FH4SeCvSGCnGAu/s0wJvhBWM5VjQp7Jjmcfunp+R3vJibqU8gAVZU1nYRLaqprLdIXrSyP2Km8nQ==} + '@sentry-internal/replay@8.20.0': + resolution: {integrity: sha512-sCiI7SOAHq5XsxkixtoMofeSyKd/hVgDV+4145f6nN9m7nLzig4PBQwh2SgK2piJ2mfaXfqcdzA1pShPYldaJA==} engines: {node: '>=14.18'} '@sentry/babel-plugin-component-annotate@2.20.1': resolution: {integrity: sha512-4mhEwYTK00bIb5Y9UWIELVUfru587Vaeg0DQGswv4aIRHIiMKLyNqCEejaaybQ/fNChIZOKmvyqXk430YVd7Qg==} engines: {node: '>= 14'} - '@sentry/browser@8.13.0': - resolution: {integrity: sha512-/tp7HZ5qjwDLtwooPMoexdAi2PG7gMNY0bHeMlwy20hs8mclC8RW8ZiJA6czXHfgnbmvxfrHaY53IJyz//JnlA==} + '@sentry/browser@8.20.0': + resolution: {integrity: sha512-JDZbCreY44/fHYN28QzsAwEHXa2rc1hzM6GE4RSlXCdAhNfrjVxyYDxhw/50pVEHZg1WXxf7ZmERjocV5VJHsw==} engines: {node: '>=14.18'} '@sentry/bundler-plugin-core@2.20.1': @@ -2128,8 +2128,8 @@ packages: engines: {node: '>= 10'} hasBin: true - '@sentry/core@8.13.0': - resolution: {integrity: sha512-N9Qg4ZGxZWp8eb2eUUHVVKgjBLtFIjS805nG92s6yJmkvOpKm6mLtcUaT/iDf3Hta6nG+xRkhbE3r+Z4cbXG8w==} + '@sentry/core@8.20.0': + resolution: {integrity: sha512-R81snuw+67VT4aCxr6ShST/s0Y6FlwN2YczhDwaGyzumn5rlvA6A4JtQDeExduNoDDyv4T3LrmW8wlYZn3CJJw==} engines: {node: '>=14.18'} '@sentry/hub@6.19.7': @@ -2140,8 +2140,8 @@ packages: resolution: {integrity: sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ==} engines: {node: '>=6'} - '@sentry/nextjs@8.13.0': - resolution: {integrity: sha512-zXZWCA/sfGVP3MEGrshUZiMM5eOu33o8vDTKExsmGRWGTsR1tkLyLUwxQQSE9PdihUnPqv/Nw27eMXZv2XpJMw==} + '@sentry/nextjs@8.20.0': + resolution: {integrity: sha512-ZMi50qeklxibnNehlghNvlmzz1NIvYUGglDMy/m/N67SfXiq5PXyVziJAoCKQXR7nrvoQx0Mx17Z9ZFIwgjSJQ==} engines: {node: '>=14.18'} peerDependencies: next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 @@ -2150,12 +2150,12 @@ packages: webpack: optional: true - '@sentry/node@8.13.0': - resolution: {integrity: sha512-OeZ7K90RhyxfwfreerIi4cszzHrPRRH36STJno2+p3sIGbG5VScOccqXzYEOAqHpByxnti4KQN34BLAT2BFOEA==} + '@sentry/node@8.20.0': + resolution: {integrity: sha512-i4ywT2m0Gw65U3uwI4NwiNcyqp9YF6/RsusfH1pg4YkiL/RYp7FS0MPVgMggfvoue9S3KjCgRVlzTLwFATyPXQ==} engines: {node: '>=14.18'} - '@sentry/opentelemetry@8.13.0': - resolution: {integrity: sha512-NYn/HNE/SxFXe8pfnxJknhrrRzYRMHNssCoi5M1CeR5G7F2BGxxVmaGsd8j0WyTCpUS4i97G4vhYtDGxHvWN6w==} + '@sentry/opentelemetry@8.20.0': + resolution: {integrity: sha512-NFcLK6+t9wUc4HlGKeuDn6W4KjZxZfZmWlrK2/tgC5KzG1cnVeOnWUrJzGHTa+YDDdIijpjiFUcpXGPkX3rmIg==} engines: {node: '>=14.18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -2164,8 +2164,8 @@ packages: '@opentelemetry/sdk-trace-base': ^1.25.1 '@opentelemetry/semantic-conventions': ^1.25.1 - '@sentry/react@8.13.0': - resolution: {integrity: sha512-gz+aHZMcl6uvHkmLBGzMGjJJ+Vpl+W0VXJsKB9fdjZDDF5vJpgXTR9mwMEXJ9lKi+cY6tDe0+af+DA8BGJgw0Q==} + '@sentry/react@8.20.0': + resolution: {integrity: sha512-vqA0o9ysdfA24/ADhsJwsmCNdUWRu2ycmVN1Sr76v+ZggyOCFzE7XD13kbqk1G3jPb8nptNu/6Zwpcy5pP4mtw==} engines: {node: '>=14.18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x @@ -2174,20 +2174,20 @@ packages: resolution: {integrity: sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==} engines: {node: '>=6'} - '@sentry/types@8.13.0': - resolution: {integrity: sha512-r63s/H5gvQnQM9tTGBXz2xErUbxZALh4e2Lg/1aHj4zIvGLBjA2z5qWsh6TEZYbpmgAyGShLDr6+rWeUVf9yBQ==} + '@sentry/types@8.20.0': + resolution: {integrity: sha512-6IP278KojOpiAA7vrd1hjhUyn26cl0n0nGsShzic5ztCVs92sTeVRnh7MTB9irDVtAbOEyt/YH6go3h+Jia1pA==} engines: {node: '>=14.18'} '@sentry/utils@6.19.7': resolution: {integrity: sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA==} engines: {node: '>=6'} - '@sentry/utils@8.13.0': - resolution: {integrity: sha512-PxV0v9VbGWH9zP37P5w2msLUFDr287nYjoY2XVF+RSolyiTs1CQNI5ZMUO3o4MsSac/dpXxjyrZXQd72t/jRYA==} + '@sentry/utils@8.20.0': + resolution: {integrity: sha512-+1I5H8dojURiEUGPliDwheQk8dhjp8uV1sMccR/W/zjFrt4wZyPs+Ttp/V7gzm9LDJoNek9tmELert/jQqWTgg==} engines: {node: '>=14.18'} - '@sentry/vercel-edge@8.13.0': - resolution: {integrity: sha512-6i2FTpIec/o+lfdtzXRIebo38ca4DWHXmpzfFZJVcYdBFWbu2F3Q6c61tnXGuZhyRjGtEW1ASPlbd8nterQIwQ==} + '@sentry/vercel-edge@8.20.0': + resolution: {integrity: sha512-4UiK72M9mf3++YapeIdwUcF0d1uzWfgYm8fx3YgEz6bQUdrts3Jg4e+dbvpv57uUAiTnNN3JKZmkT1ep9ZonKw==} engines: {node: '>=14.18'} '@sentry/webpack-plugin@2.20.1': @@ -2300,9 +2300,6 @@ packages: '@tsconfig/recommended@1.0.6': resolution: {integrity: sha512-0IKu9GHYF1NGTJiYgfWwqnOQSlnE9V9R7YohHNNf0/fj/SyOZWzdd06JFr0fLpg1Mqw0kGbYg8w5xdkSqLKM9g==} - '@types/accepts@1.3.7': - resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2315,21 +2312,9 @@ packages: '@types/babel__traverse@7.20.4': resolution: {integrity: sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==} - '@types/body-parser@1.19.5': - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/content-disposition@0.5.8': - resolution: {integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==} - - '@types/cookies@0.9.0': - resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==} - '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -2339,12 +2324,6 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/express-serve-static-core@4.19.5': - resolution: {integrity: sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==} - - '@types/express@4.17.21': - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} @@ -2354,15 +2333,9 @@ packages: '@types/hoist-non-react-statics@3.3.5': resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} - '@types/http-assert@1.5.5': - resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==} - '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -2381,21 +2354,6 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - '@types/keygrip@1.0.6': - resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} - - '@types/koa-compose@3.2.8': - resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==} - - '@types/koa@2.14.0': - resolution: {integrity: sha512-DTDUyznHGNHAl+wd1n0z1jxNajduyTh8R53xoewuerdBzGo6Ogj6F2299BFtrexJw4NtgjsI5SMPCmV9gZwGXA==} - - '@types/koa__router@12.0.3': - resolution: {integrity: sha512-5YUJVv6NwM1z7m6FuYpKfNLTZ932Z6EF6xy2BbtpJSyn13DKNQEkXVffFVSnJHxvwwWh2SAeumpjAYUELqgjyw==} - - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/mysql@2.15.22': resolution: {integrity: sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==} @@ -2429,12 +2387,6 @@ packages: '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} - '@types/qs@6.9.15': - resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} @@ -2447,12 +2399,6 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - '@types/send@0.17.4': - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - - '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/shimmer@1.0.5': resolution: {integrity: sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==} @@ -4043,11 +3989,11 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} - import-in-the-middle@1.4.2: - resolution: {integrity: sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==} + import-in-the-middle@1.10.0: + resolution: {integrity: sha512-Z1jumVdF2GwnnYfM0a/y2ts7mZbwFMgt5rRuVmLgobgahC6iKgN5MBuXjzfTIOUpq5LSU10vJIPpVKe0X89fIw==} - import-in-the-middle@1.8.1: - resolution: {integrity: sha512-yhRwoHtiLGvmSozNOALgjRPFI6uYsds60EoMqqnXyyv+JOIW/BrrLejuTGBt+bq0T5tLzOHrN0T7xYTm4Qt/ng==} + import-in-the-middle@1.7.1: + resolution: {integrity: sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg==} import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} @@ -5023,9 +4969,11 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} - opentelemetry-instrumentation-fetch-node@1.2.0: - resolution: {integrity: sha512-aiSt/4ubOTyb1N5C2ZbGrBvaJOXIZhZvpRPYuUVxQJe27wJZqf/o65iPrqgLcgfeOLaQ8cS2Q+762jrYvniTrA==} + opentelemetry-instrumentation-fetch-node@1.2.3: + resolution: {integrity: sha512-Qb11T7KvoCevMaSeuamcLsAD+pZnavkhDnlVL0kRozfhl42dKG5Q3anUklAFKJZjY3twLR+BnRa6DlwwkIE/+A==} engines: {node: '>18.0.0'} + peerDependencies: + '@opentelemetry/api': ^1.6.0 optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -6506,7 +6454,7 @@ snapshots: '@babel/helpers': 7.24.4 '@babel/parser': 7.24.4 '@babel/template': 7.24.0 - '@babel/traverse': 7.24.1(supports-color@5.5.0) + '@babel/traverse': 7.24.1 '@babel/types': 7.24.0 convert-source-map: 2.0.0 debug: 4.3.5(supports-color@5.5.0) @@ -7584,6 +7532,21 @@ snapshots: '@babel/parser': 7.24.7 '@babel/types': 7.24.7 + '@babel/traverse@7.24.1': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.5(supports-color@5.5.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/traverse@7.24.1(supports-color@5.5.0)': dependencies: '@babel/code-frame': 7.24.2 @@ -8644,7 +8607,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.25.1 - '@opentelemetry/instrumentation-connect@0.37.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-connect@0.38.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) @@ -8654,7 +8617,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-express@0.40.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-express@0.41.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) @@ -8663,7 +8626,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-fastify@0.37.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-fastify@0.38.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) @@ -8672,14 +8635,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-graphql@0.41.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-graphql@0.42.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-hapi@0.39.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-hapi@0.40.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) @@ -8698,7 +8661,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.41.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.42.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) @@ -8707,18 +8670,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-koa@0.41.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-koa@0.42.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 - '@types/koa': 2.14.0 - '@types/koa__router': 12.0.3 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongodb@0.45.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mongodb@0.46.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) @@ -8727,7 +8688,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongoose@0.39.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mongoose@0.40.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) @@ -8736,7 +8697,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql2@0.39.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mysql2@0.40.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) @@ -8745,7 +8706,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql@0.39.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mysql@0.40.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) @@ -8754,7 +8715,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.38.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-nestjs-core@0.39.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) @@ -8762,7 +8723,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.42.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.43.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) @@ -8773,7 +8734,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-redis-4@0.40.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-redis-4@0.41.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) @@ -8782,11 +8743,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.43.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.46.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@types/shimmer': 1.0.5 - import-in-the-middle: 1.4.2 + import-in-the-middle: 1.7.1 require-in-the-middle: 7.3.0 semver: 7.6.3 shimmer: 1.2.1 @@ -8799,7 +8760,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.52.1 '@types/shimmer': 1.0.5 - import-in-the-middle: 1.8.1 + import-in-the-middle: 1.10.0 require-in-the-middle: 7.3.0 semver: 7.6.3 shimmer: 1.2.1 @@ -8854,7 +8815,7 @@ snapshots: '@popperjs/core@2.11.8': {} - '@prisma/instrumentation@5.16.0': + '@prisma/instrumentation@5.17.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) @@ -9180,43 +9141,43 @@ snapshots: optionalDependencies: rollup: 3.29.4 - '@sentry-internal/browser-utils@8.13.0': + '@sentry-internal/browser-utils@8.20.0': dependencies: - '@sentry/core': 8.13.0 - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 - '@sentry-internal/feedback@8.13.0': + '@sentry-internal/feedback@8.20.0': dependencies: - '@sentry/core': 8.13.0 - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 - '@sentry-internal/replay-canvas@8.13.0': + '@sentry-internal/replay-canvas@8.20.0': dependencies: - '@sentry-internal/replay': 8.13.0 - '@sentry/core': 8.13.0 - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry-internal/replay': 8.20.0 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 - '@sentry-internal/replay@8.13.0': + '@sentry-internal/replay@8.20.0': dependencies: - '@sentry-internal/browser-utils': 8.13.0 - '@sentry/core': 8.13.0 - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry-internal/browser-utils': 8.20.0 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 '@sentry/babel-plugin-component-annotate@2.20.1': {} - '@sentry/browser@8.13.0': + '@sentry/browser@8.20.0': dependencies: - '@sentry-internal/browser-utils': 8.13.0 - '@sentry-internal/feedback': 8.13.0 - '@sentry-internal/replay': 8.13.0 - '@sentry-internal/replay-canvas': 8.13.0 - '@sentry/core': 8.13.0 - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry-internal/browser-utils': 8.20.0 + '@sentry-internal/feedback': 8.20.0 + '@sentry-internal/replay': 8.20.0 + '@sentry-internal/replay-canvas': 8.20.0 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 '@sentry/bundler-plugin-core@2.20.1': dependencies: @@ -9272,10 +9233,10 @@ snapshots: - encoding - supports-color - '@sentry/core@8.13.0': + '@sentry/core@8.20.0': dependencies: - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 '@sentry/hub@6.19.7': dependencies: @@ -9289,17 +9250,18 @@ snapshots: '@sentry/types': 6.19.7 tslib: 1.14.1 - '@sentry/nextjs@8.13.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1)': + '@sentry/nextjs@8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1)': dependencies: '@opentelemetry/instrumentation-http': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 '@rollup/plugin-commonjs': 26.0.1(rollup@3.29.4) - '@sentry/core': 8.13.0 - '@sentry/node': 8.13.0 - '@sentry/opentelemetry': 8.13.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1) - '@sentry/react': 8.13.0(react@18.3.1) - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 - '@sentry/vercel-edge': 8.13.0 + '@sentry/core': 8.20.0 + '@sentry/node': 8.20.0 + '@sentry/opentelemetry': 8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1) + '@sentry/react': 8.20.0(react@18.3.1) + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 + '@sentry/vercel-edge': 8.20.0 '@sentry/webpack-plugin': 2.20.1(webpack@5.92.1) chalk: 3.0.0 next: 14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -9313,83 +9275,83 @@ snapshots: - '@opentelemetry/core' - '@opentelemetry/instrumentation' - '@opentelemetry/sdk-trace-base' - - '@opentelemetry/semantic-conventions' - encoding - react - supports-color - '@sentry/node@8.13.0': + '@sentry/node@8.20.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.37.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.40.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fastify': 0.37.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.41.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.39.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.38.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.38.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.40.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.41.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.41.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.45.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.39.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.39.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.39.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-nestjs-core': 0.38.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.42.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis-4': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.39.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.41.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 - '@prisma/instrumentation': 5.16.0 - '@sentry/core': 8.13.0 - '@sentry/opentelemetry': 8.13.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1) - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@prisma/instrumentation': 5.17.0 + '@sentry/core': 8.20.0 + '@sentry/opentelemetry': 8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1) + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 + import-in-the-middle: 1.10.0 optionalDependencies: - opentelemetry-instrumentation-fetch-node: 1.2.0 + opentelemetry-instrumentation-fetch-node: 1.2.3(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@sentry/opentelemetry@8.13.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)': + '@sentry/opentelemetry@8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 - '@sentry/core': 8.13.0 - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 - '@sentry/react@8.13.0(react@18.3.1)': + '@sentry/react@8.20.0(react@18.3.1)': dependencies: - '@sentry/browser': 8.13.0 - '@sentry/core': 8.13.0 - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry/browser': 8.20.0 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 hoist-non-react-statics: 3.3.2 react: 18.3.1 '@sentry/types@6.19.7': {} - '@sentry/types@8.13.0': {} + '@sentry/types@8.20.0': {} '@sentry/utils@6.19.7': dependencies: '@sentry/types': 6.19.7 tslib: 1.14.1 - '@sentry/utils@8.13.0': + '@sentry/utils@8.20.0': dependencies: - '@sentry/types': 8.13.0 + '@sentry/types': 8.20.0 - '@sentry/vercel-edge@8.13.0': + '@sentry/vercel-edge@8.20.0': dependencies: - '@sentry/core': 8.13.0 - '@sentry/types': 8.13.0 - '@sentry/utils': 8.13.0 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 '@sentry/webpack-plugin@2.20.1(webpack@5.92.1)': dependencies: @@ -9515,10 +9477,6 @@ snapshots: '@tsconfig/recommended@1.0.6': {} - '@types/accepts@1.3.7': - dependencies: - '@types/node': 20.14.11 - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.24.7 @@ -9540,28 +9498,10 @@ snapshots: dependencies: '@babel/types': 7.24.7 - '@types/body-parser@1.19.5': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 20.14.11 - '@types/connect@3.4.36': dependencies: '@types/node': 20.14.11 - '@types/connect@3.4.38': - dependencies: - '@types/node': 20.14.11 - - '@types/content-disposition@0.5.8': {} - - '@types/cookies@0.9.0': - dependencies: - '@types/connect': 3.4.38 - '@types/express': 4.17.21 - '@types/keygrip': 1.0.6 - '@types/node': 20.14.11 - '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 8.56.10 @@ -9574,20 +9514,6 @@ snapshots: '@types/estree@1.0.5': {} - '@types/express-serve-static-core@4.19.5': - dependencies: - '@types/node': 20.14.11 - '@types/qs': 6.9.15 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - - '@types/express@4.17.21': - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.5 - '@types/qs': 6.9.15 - '@types/serve-static': 1.15.7 - '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 @@ -9603,12 +9529,8 @@ snapshots: '@types/react': 18.3.3 hoist-non-react-statics: 3.3.2 - '@types/http-assert@1.5.5': {} - '@types/http-cache-semantics@4.0.4': {} - '@types/http-errors@2.0.4': {} - '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -9631,29 +9553,6 @@ snapshots: '@types/node': 20.14.11 optional: true - '@types/keygrip@1.0.6': {} - - '@types/koa-compose@3.2.8': - dependencies: - '@types/koa': 2.14.0 - - '@types/koa@2.14.0': - dependencies: - '@types/accepts': 1.3.7 - '@types/content-disposition': 0.5.8 - '@types/cookies': 0.9.0 - '@types/http-assert': 1.5.5 - '@types/http-errors': 2.0.4 - '@types/keygrip': 1.0.6 - '@types/koa-compose': 3.2.8 - '@types/node': 20.14.11 - - '@types/koa__router@12.0.3': - dependencies: - '@types/koa': 2.14.0 - - '@types/mime@1.3.5': {} - '@types/mysql@2.15.22': dependencies: '@types/node': 20.14.11 @@ -9690,10 +9589,6 @@ snapshots: '@types/prop-types@15.7.12': {} - '@types/qs@6.9.15': {} - - '@types/range-parser@1.2.7': {} - '@types/react-dom@18.3.0': dependencies: '@types/react': 18.3.3 @@ -9709,17 +9604,6 @@ snapshots: '@types/semver@7.5.8': {} - '@types/send@0.17.4': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.14.11 - - '@types/serve-static@1.15.7': - dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 20.14.11 - '@types/send': 0.17.4 - '@types/shimmer@1.0.5': {} '@types/stack-utils@2.0.3': {} @@ -11574,20 +11458,20 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-in-the-middle@1.4.2: + import-in-the-middle@1.10.0: dependencies: acorn: 8.12.1 - acorn-import-assertions: 1.9.0(acorn@8.12.1) + acorn-import-attributes: 1.9.5(acorn@8.12.1) cjs-module-lexer: 1.2.3 module-details-from-path: 1.0.3 - optional: true - import-in-the-middle@1.8.1: + import-in-the-middle@1.7.1: dependencies: acorn: 8.12.1 - acorn-import-attributes: 1.9.5(acorn@8.12.1) + acorn-import-assertions: 1.9.0(acorn@8.12.1) cjs-module-lexer: 1.2.3 module-details-from-path: 1.0.3 + optional: true import-lazy@4.0.0: {} @@ -12776,10 +12660,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - opentelemetry-instrumentation-fetch-node@1.2.0: + opentelemetry-instrumentation-fetch-node@1.2.3(@opentelemetry/api@1.9.0): dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color From c226c4861ca7b37461e5da8d626233a770afb35e Mon Sep 17 00:00:00 2001 From: ofreyssinet-ledger Date: Mon, 29 Jul 2024 10:52:26 +0200 Subject: [PATCH 11/46] =?UTF-8?q?=E2=9C=A8=20(core):=20Add=20Flex=20device?= =?UTF-8?q?=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/api/device/DeviceModel.ts | 1 + .../data/StaticDeviceModelDataSource.test.ts | 12 +++++++++++- .../data/StaticDeviceModelDataSource.ts | 17 +++++++++++++++++ .../device-model/model/DeviceModel.test.ts | 11 +++++++++++ .../internal/device-model/model/DeviceModel.ts | 4 ++-- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/core/src/api/device/DeviceModel.ts b/packages/core/src/api/device/DeviceModel.ts index d17ba56e1..c2fefbf03 100644 --- a/packages/core/src/api/device/DeviceModel.ts +++ b/packages/core/src/api/device/DeviceModel.ts @@ -3,6 +3,7 @@ export enum DeviceModelId { NANO_SP = "nanoSP", NANO_X = "nanoX", STAX = "stax", + FLEX = "flex", } /** diff --git a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts b/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts index d3d1c76e5..360c7cf36 100644 --- a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts +++ b/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts @@ -9,7 +9,7 @@ describe("StaticDeviceModelDataSource", () => { const deviceModels = dataSource.getAllDeviceModels(); // Currently supporting 4 device models - expect(deviceModels.length).toEqual(4); + expect(deviceModels.length).toEqual(5); expect(deviceModels).toContainEqual( expect.objectContaining({ id: DeviceModelId.NANO_S }), ); @@ -22,6 +22,9 @@ describe("StaticDeviceModelDataSource", () => { expect(deviceModels).toContainEqual( expect.objectContaining({ id: DeviceModelId.STAX }), ); + expect(deviceModels).toContainEqual( + expect.objectContaining({ id: DeviceModelId.FLEX }), + ); }); }); @@ -56,6 +59,13 @@ describe("StaticDeviceModelDataSource", () => { expect(deviceModel4).toEqual( expect.objectContaining({ id: DeviceModelId.STAX }), ); + + const deviceModel5 = dataSource.getDeviceModel({ + id: DeviceModelId.FLEX, + }); + expect(deviceModel5).toEqual( + expect.objectContaining({ id: DeviceModelId.FLEX }), + ); }); }); diff --git a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts b/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts index b02c8fc70..d29f406b7 100644 --- a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts +++ b/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts @@ -65,6 +65,23 @@ export class StaticDeviceModelDataSource implements DeviceModelDataSource { }, ], }), + [DeviceModelId.FLEX]: new InternalDeviceModel({ + id: DeviceModelId.FLEX, + productName: "Ledger Flex", + usbProductId: 0x70, + legacyUsbProductId: 0x0007, + usbOnly: false, + memorySize: 1533 * 1024, + masks: [0x33300000], + bluetoothSpec: [ + { + serviceUuid: "13d63400-2c97-3004-0000-4c6564676572", + notifyUuid: "13d63400-2c97-3004-0001-4c6564676572", + writeUuid: "13d63400-2c97-3004-0002-4c6564676572", + writeCmdUuid: "13d63400-2c97-3004-0003-4c6564676572", + }, + ], + }), }; getAllDeviceModels(): InternalDeviceModel[] { diff --git a/packages/core/src/internal/device-model/model/DeviceModel.test.ts b/packages/core/src/internal/device-model/model/DeviceModel.test.ts index 457011d4d..323420f8d 100644 --- a/packages/core/src/internal/device-model/model/DeviceModel.test.ts +++ b/packages/core/src/internal/device-model/model/DeviceModel.test.ts @@ -56,4 +56,15 @@ describe("DeviceModel", () => { expect(deviceModel.getBlockSize(firmwareVersion)).toBe(2 * 1024); }); + + // flex + test("should return the correct block size for Flex", () => { + const deviceModel = new InternalDeviceModel({ + ...stubDeviceModel, + id: DeviceModelId.FLEX, + }); + const firmwareVersion = "2.0.0"; + + expect(deviceModel.getBlockSize(firmwareVersion)).toBe(32); + }); }); diff --git a/packages/core/src/internal/device-model/model/DeviceModel.ts b/packages/core/src/internal/device-model/model/DeviceModel.ts index 96dc07cc0..b1019de32 100644 --- a/packages/core/src/internal/device-model/model/DeviceModel.ts +++ b/packages/core/src/internal/device-model/model/DeviceModel.ts @@ -52,11 +52,11 @@ export class InternalDeviceModel { return semver.lt(semver.coerce(firmwareVersion) ?? "", "2.0.0") ? 4 * 1024 : 2 * 1024; - case DeviceModelId.NANO_SP: - return 32; case DeviceModelId.NANO_X: return 4 * 1024; + case DeviceModelId.NANO_SP: case DeviceModelId.STAX: + case DeviceModelId.FLEX: return 32; } } From f8a56d8895ab897c6f16f823eb7a407807b6d99e Mon Sep 17 00:00:00 2001 From: ofreyssinet-ledger Date: Mon, 29 Jul 2024 10:53:15 +0200 Subject: [PATCH 12/46] =?UTF-8?q?=E2=9C=A8=20(sample):=20Use=20correct=20i?= =?UTF-8?q?con=20for=20Flex=20device=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sample/package.json | 2 +- apps/sample/src/components/Device/index.tsx | 18 ++++++--- pnpm-lock.yaml | 43 +++++++-------------- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/apps/sample/package.json b/apps/sample/package.json index cf537ae1c..8fb27e123 100644 --- a/apps/sample/package.json +++ b/apps/sample/package.json @@ -15,7 +15,7 @@ "dependencies": { "@ledgerhq/device-sdk-core": "workspace:*", "@ledgerhq/keyring-eth": "workspace:*", - "@ledgerhq/react-ui": "^0.15.1", + "@ledgerhq/react-ui": "^0.15.3", "@sentry/nextjs": "^8.20.0", "next": "14.2.4", "react": "^18.3.1", diff --git a/apps/sample/src/components/Device/index.tsx b/apps/sample/src/components/Device/index.tsx index 3a2d8c587..6ce188217 100644 --- a/apps/sample/src/components/Device/index.tsx +++ b/apps/sample/src/components/Device/index.tsx @@ -43,6 +43,17 @@ type DeviceProps = { onDisconnect: () => Promise; }; +function getIconComponent(model: DeviceModelId) { + switch (model) { + case DeviceModelId.STAX: + return Icons.Stax; + case DeviceModelId.FLEX: + return Icons.Flex; + default: + return Icons.Nano; + } +} + export const Device: React.FC = ({ name, type, @@ -51,14 +62,11 @@ export const Device: React.FC = ({ sessionId, }) => { const sessionState = useDeviceSessionState(sessionId); + const IconComponent = getIconComponent(model); return ( - {model === DeviceModelId.STAX ? ( - - ) : ( - - )} + {name} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c522a072..960628d7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,8 +69,8 @@ importers: specifier: workspace:* version: link:../../packages/signer/keyring-eth '@ledgerhq/react-ui': - specifier: ^0.15.1 - version: 0.15.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)) + specifier: ^0.15.3 + version: 0.15.3(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)) '@sentry/nextjs': specifier: ^8.20.0 version: 8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1) @@ -1588,8 +1588,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@ledgerhq/crypto-icons-ui@1.2.0': - resolution: {integrity: sha512-suazaJ7dTh2gMowH1NklxPpdgv2zbulcdWow9ASlnsHpHg9JGMT7y+G+9eEr1+aVX855rVFXgcp1YgpWvUVBwg==} + '@ledgerhq/crypto-icons-ui@1.3.0': + resolution: {integrity: sha512-m1n8iMIe+g5ZvkZ7VMVnqXTnRl7zxtj6RYRhehSshrNk6Lth8ZpOFthGVluge/2PYAdtc5FdImzmUHiz2fo7Wg==} peerDependencies: '@types/react': '*' react: '*' @@ -1600,8 +1600,8 @@ packages: '@types/react': optional: true - '@ledgerhq/icons-ui@0.7.0': - resolution: {integrity: sha512-4Jn1sAuCKGG2CLmPm2W9hn/xcBoZS8Hzw4c/cJAzUq8QfRjCSEmPyk73PPCgHIQjc2AkO2qEw1zFdEfWeXocFw==} + '@ledgerhq/icons-ui@0.7.1': + resolution: {integrity: sha512-VTfyz8X4GN+ZwS+7BTCJkeSpNMS6ob8XvhUS/J7zRtKAGj7z0Yy1nMuWgYw8rL4NfUYpZnYbShtVVjsEK2uylA==} peerDependencies: '@types/react': '*' react: '*' @@ -1612,8 +1612,8 @@ packages: '@types/react': optional: true - '@ledgerhq/react-ui@0.15.1': - resolution: {integrity: sha512-b58pgKwGnR/WWfpymwRqdjuTlazSOC1dMgQSZKOL4HkYggj0FoXC8PECaC8cICS2ygSJG6mlw1RuZK+IAnCrIg==} + '@ledgerhq/react-ui@0.15.3': + resolution: {integrity: sha512-12h8+PGAevUf+1Xs7vuIjl7JTn+eQPS1yoZS+t26/hnCB0DmgdtukfXEVWcyMRvLD7nLnJWB2bX3qSZ4wjCTVA==} peerDependencies: '@types/react': '*' react: '>=17.0.2' @@ -6454,7 +6454,7 @@ snapshots: '@babel/helpers': 7.24.4 '@babel/parser': 7.24.4 '@babel/template': 7.24.0 - '@babel/traverse': 7.24.1 + '@babel/traverse': 7.24.1(supports-color@5.5.0) '@babel/types': 7.24.0 convert-source-map: 2.0.0 debug: 4.3.5(supports-color@5.5.0) @@ -7532,21 +7532,6 @@ snapshots: '@babel/parser': 7.24.7 '@babel/types': 7.24.7 - '@babel/traverse@7.24.1': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 - debug: 4.3.5(supports-color@5.5.0) - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.24.1(supports-color@5.5.0)': dependencies: '@babel/code-frame': 7.24.2 @@ -8404,7 +8389,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@ledgerhq/crypto-icons-ui@1.2.0(@types/react@18.3.3)(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(styled-system@5.1.5)': + '@ledgerhq/crypto-icons-ui@1.3.0(@types/react@18.3.3)(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(styled-system@5.1.5)': dependencies: react: 18.3.1 react-native-svg: 15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1) @@ -8413,7 +8398,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - '@ledgerhq/icons-ui@0.7.0(@types/react@18.3.3)(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(styled-system@5.1.5)': + '@ledgerhq/icons-ui@0.7.1(@types/react@18.3.3)(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(styled-system@5.1.5)': dependencies: react: 18.3.1 react-native-svg: 15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1) @@ -8422,11 +8407,11 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - '@ledgerhq/react-ui@0.15.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))': + '@ledgerhq/react-ui@0.15.3(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))': dependencies: '@floating-ui/react-dom': 0.4.3(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ledgerhq/crypto-icons-ui': 1.2.0(@types/react@18.3.3)(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(styled-system@5.1.5) - '@ledgerhq/icons-ui': 0.7.0(@types/react@18.3.3)(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(styled-system@5.1.5) + '@ledgerhq/crypto-icons-ui': 1.3.0(@types/react@18.3.3)(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(styled-system@5.1.5) + '@ledgerhq/icons-ui': 0.7.1(@types/react@18.3.3)(react-native-svg@15.3.0(react-native@0.74.3(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(styled-system@5.1.5) '@ledgerhq/ui-shared': 0.2.1 '@tippyjs/react': 4.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) color: 4.2.3 From d9e0164d69bede69269d0989c24a8631b9a0875d Mon Sep 17 00:00:00 2001 From: Olivier Freyssinet Date: Mon, 29 Jul 2024 10:53:46 +0200 Subject: [PATCH 13/46] =?UTF-8?q?=F0=9F=94=96=20(chore):=20Changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/proud-turtles-tease.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/proud-turtles-tease.md diff --git a/.changeset/proud-turtles-tease.md b/.changeset/proud-turtles-tease.md new file mode 100644 index 000000000..974f41c7c --- /dev/null +++ b/.changeset/proud-turtles-tease.md @@ -0,0 +1,6 @@ +--- +"@ledgerhq/device-sdk-core": patch +"@ledgerhq/device-sdk-sample": patch +--- + +Add support of Ledger Flex From c68bc7563c71e89460f1a64c257f122a25ff93dc Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Tue, 30 Jul 2024 16:51:43 +0200 Subject: [PATCH 14/46] =?UTF-8?q?=E2=9C=A8=20(keyring-eth):=20Implement=20?= =?UTF-8?q?ProvideTokenInformationCommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProvideTokenInformationCommand.test.ts | 100 ++++++++++++++++++ .../command/ProvideTokenInformationCommand.ts | 50 ++++++++- 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/ProvideTokenInformationCommand.test.ts diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideTokenInformationCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideTokenInformationCommand.test.ts new file mode 100644 index 000000000..2929d379a --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideTokenInformationCommand.test.ts @@ -0,0 +1,100 @@ +import { Command, InvalidStatusWordError } from "@ledgerhq/device-sdk-core"; + +import { + ProvideTokenInformationCommand, + ProvideTokenInformationCommandArgs, +} from "./ProvideTokenInformationCommand"; + +const PAYLOAD_USDT = + "0455534454dac17f958d2ee523a2206206994597c13d831ec700000006000000013044022078c66ccea3e4dedb15a24ec3c783d7b582cd260daf62fd36afe9a8212a344aed0220160ba8c1c4b6a8aa6565bed20632a091aeeeb7bfdac67fc6589a6031acbf511c"; + +const PAYLOAD_USDC = + "0455534443a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000006000000013045022100b2e358726e4e6a6752cf344017c0e9d45b9a904120758d45f61b2804f9ad5299022015161ef28d8c4481bd9432c13562def9cce688bcfec896ef244c9a213f106cdd"; + +const PROVIDE_TOKEN_INFORMATION_APDU_USDT = Uint8Array.from([ + 0xe0, 0x0a, 0x00, 0x00, 0x67, 0x04, 0x55, 0x53, 0x44, 0x54, 0xda, 0xc1, 0x7f, + 0x95, 0x8d, 0x2e, 0xe5, 0x23, 0xa2, 0x20, 0x62, 0x06, 0x99, 0x45, 0x97, 0xc1, + 0x3d, 0x83, 0x1e, 0xc7, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x30, + 0x44, 0x02, 0x20, 0x78, 0xc6, 0x6c, 0xce, 0xa3, 0xe4, 0xde, 0xdb, 0x15, 0xa2, + 0x4e, 0xc3, 0xc7, 0x83, 0xd7, 0xb5, 0x82, 0xcd, 0x26, 0x0d, 0xaf, 0x62, 0xfd, + 0x36, 0xaf, 0xe9, 0xa8, 0x21, 0x2a, 0x34, 0x4a, 0xed, 0x02, 0x20, 0x16, 0x0b, + 0xa8, 0xc1, 0xc4, 0xb6, 0xa8, 0xaa, 0x65, 0x65, 0xbe, 0xd2, 0x06, 0x32, 0xa0, + 0x91, 0xae, 0xee, 0xb7, 0xbf, 0xda, 0xc6, 0x7f, 0xc6, 0x58, 0x9a, 0x60, 0x31, + 0xac, 0xbf, 0x51, 0x1c, +]); + +const PROVIDE_TOKEN_INFORMATION_APDU_USDC = Uint8Array.from([ + 0xe0, 0x0a, 0x00, 0x00, 0x68, 0x04, 0x55, 0x53, 0x44, 0x43, 0xa0, 0xb8, 0x69, + 0x91, 0xc6, 0x21, 0x8b, 0x36, 0xc1, 0xd1, 0x9d, 0x4a, 0x2e, 0x9e, 0xb0, 0xce, + 0x36, 0x06, 0xeb, 0x48, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x30, + 0x45, 0x02, 0x21, 0x00, 0xb2, 0xe3, 0x58, 0x72, 0x6e, 0x4e, 0x6a, 0x67, 0x52, + 0xcf, 0x34, 0x40, 0x17, 0xc0, 0xe9, 0xd4, 0x5b, 0x9a, 0x90, 0x41, 0x20, 0x75, + 0x8d, 0x45, 0xf6, 0x1b, 0x28, 0x04, 0xf9, 0xad, 0x52, 0x99, 0x02, 0x20, 0x15, + 0x16, 0x1e, 0xf2, 0x8d, 0x8c, 0x44, 0x81, 0xbd, 0x94, 0x32, 0xc1, 0x35, 0x62, + 0xde, 0xf9, 0xcc, 0xe6, 0x88, 0xbc, 0xfe, 0xc8, 0x96, 0xef, 0x24, 0x4c, 0x9a, + 0x21, 0x3f, 0x10, 0x6c, 0xdd, +]); + +describe("ProvideTokenInformationCommand", () => { + let command: Command; + + describe("getApdu", () => { + it("should return the apdu for usdt payload", () => { + // GIVEN + command = new ProvideTokenInformationCommand({ payload: PAYLOAD_USDT }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + PROVIDE_TOKEN_INFORMATION_APDU_USDT, + ); + }); + + it("should return the apdu for usdc payload", () => { + // GIVEN + command = new ProvideTokenInformationCommand({ payload: PAYLOAD_USDC }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + PROVIDE_TOKEN_INFORMATION_APDU_USDC, + ); + }); + }); + + describe("parseResponse", () => { + it("should parse the response", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: new Uint8Array(), + }; + + // WHEN + const parsedResponse = command.parseResponse(response); + + // THEN + expect(parsedResponse).toBeUndefined(); + }); + + it("should throw an error if the response is not successful", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x55, 0x15]), + data: new Uint8Array(), + }; + + // WHEN + const promise = () => command.parseResponse(response); + + // THEN + expect(() => { + promise(); + }).toThrow(InvalidStatusWordError); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideTokenInformationCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideTokenInformationCommand.ts index b13ecf731..7881bfee6 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideTokenInformationCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideTokenInformationCommand.ts @@ -1,2 +1,50 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#provide-erc-20-token-information -export class ProvideTokenInformationCommand {} +import { + Apdu, + ApduBuilder, + ApduBuilderArgs, + ApduParser, + ApduResponse, + Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +export type ProvideTokenInformationCommandArgs = { + payload: string; +}; + +export class ProvideTokenInformationCommand + implements Command +{ + args: ProvideTokenInformationCommandArgs; + + constructor(args: ProvideTokenInformationCommandArgs) { + this.args = args; + } + + getApdu(): Apdu { + const getEthAddressArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x0a, + p1: 0x00, + p2: 0x00, + }; + const builder = new ApduBuilder(getEthAddressArgs); + builder.addHexaStringToData(this.args.payload); + return builder.build(); + } + + parseResponse(response: ApduResponse): void { + const parser = new ApduParser(response); + + // TODO: handle the error correctly using a generic error handler + if (!CommandUtils.isSuccessResponse(response)) { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + response.statusCode, + )}`, + ); + } + } +} From 77a4938d34df103e41e8efa203ac4fc793d9d420 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Tue, 30 Jul 2024 16:00:57 +0200 Subject: [PATCH 15/46] =?UTF-8?q?=F0=9F=94=96=20(chore):=20Changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/chatty-bats-sleep.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chatty-bats-sleep.md diff --git a/.changeset/chatty-bats-sleep.md b/.changeset/chatty-bats-sleep.md new file mode 100644 index 000000000..086d7d8a4 --- /dev/null +++ b/.changeset/chatty-bats-sleep.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement ProvideTokenInformationCommand From a84adab223cf19c701049ffa73b8bd28e64b9e84 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 31 Jul 2024 12:09:08 +0200 Subject: [PATCH 16/46] =?UTF-8?q?=F0=9F=93=9D=20(context-module):=20Add=20?= =?UTF-8?q?context-module=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/signer/context-module/README.md | 119 ++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/packages/signer/context-module/README.md b/packages/signer/context-module/README.md index f8b403ce9..d9d4f081b 100644 --- a/packages/signer/context-module/README.md +++ b/packages/signer/context-module/README.md @@ -1 +1,118 @@ -# Context Module +# Ledger Context Module Implementation + +> [!CAUTION] +> This is still under development and we are free to make new interfaces which may lead to Device SDK breaking changes. + +## Introduction + +The purpose of the **Context Module** is to provide all the necessary context for the clear signing operation. +This module includes the Ledger implementation of the context module and all the default context loaders used to fetch the context of a transaction. +This open-source module can serve as an example for implementing custom context modules or loaders. + +## How does it work + +The Context Module features an interface utilized by the Signer module to retrieve the context of a transaction. This module comprises multiple loaders, each capable of being specified individually. Each loader attempts to fetch context from the backend relevant to its domain. For example, one loader retrieves information about tokens, another fetches information about NFTs, and so on. + +The following diagram illustrates the communication between the various modules when the context for a token transaction is successfully retrieved: + +```mermaid + flowchart LR; + Signer --Transaction--> ContextModule + ContextModule --Transaction--> TokenContextLoader + ContextModule --Transaction--> NftContextLoader + ContextModule --Transaction--> OtherContextLoader + TokenContextLoader --> Backend1(Backend) + NftContextLoader --> Backend2(Backend) + OtherContextLoader --> Backend3(Backend) + Backend1 --Context--> TokenContextLoader + TokenContextLoader --Context--> ContextModule + ContextModule --Context--> Signer +``` + +## Installation + +To install the context-module package, run the following command: + +```sh +npm install @ledgerhq/context-module +``` + +## Usage + +### Main Features + +It currently supports the following features: + +- Tokens: provide information about tokens used in the transaction. +- NFTs: provide information about NFTs used in the transaction. +- Domain name: provide information about domain names. +- Custom plugins: provide complex informations to external plugins such as the **1inch** or **paraswap** plugin. + +> [!NOTE] +> At the moment the context module is available only for Ethereum blockchain. + +### Setting up + +The context-module package exposes a builder `ContextModuleBuilder` which will be used to initialise the context module with your configuration. + +```ts +const contextModule = new ContextModuleBuilder().build(); +``` + +It is also possible to instantiate the context module without the default loaders. + +```ts +const contextModule = new ContextModuleBuilder() + .withoutDefaultLoaders() + .build(); +``` + +> [!NOTE] +> Without loaders, a transaction cannot be clear signed. Use it with caution. + +You can add a custom list of loader to the context module. + +```ts +// Default Token Loader +const tokenLoader = new TokenContextLoader(new TokenDataSource()); + +// Custom Loader +const myCustomLoader = new MyCustomLoader(); + +// Custom datasource for a default Token Loader +const myCustomTokenDataSource = new MyCustomTokenDataSource(); +const myTokenLoader = new TokenCOntextLoader(); + +const contextModule = new ContextModuleBuilder() + .withoutDefaultLoaders() + .addLoader(tokenLoader) + .addLoader(myTokenLoader) + .addLoader(myCustomLoader) + .build(); +``` + +### Create a custom loader + +A custom loader must implement the ContextLoader interface, defined as follows: + +```ts +type ContextLoader = { + load: (transaction: TransactionContext) => Promise; +}; +``` + +with ClearSignContextSuccess defined as follows: + +```ts +type ClearSignContextSuccess = { + type: "token" | "nft" | "domainName" | "plugin" | "externalPlugin"; + payload: string; +}; +``` + +The payload should represent the data sent to the device to provide information and must be signed by a trusted authority. + +### Errors handling + +> [!CAUTION] +> To be defined From e0734365a2cedc79aa7786038d5f47880fba4319 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Tue, 30 Jul 2024 09:47:23 +0200 Subject: [PATCH 17/46] =?UTF-8?q?=F0=9F=94=96=20(chore):=20Changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/strong-terms-brake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strong-terms-brake.md diff --git a/.changeset/strong-terms-brake.md b/.changeset/strong-terms-brake.md new file mode 100644 index 000000000..dc4ec9b99 --- /dev/null +++ b/.changeset/strong-terms-brake.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/context-module": patch +--- + +Update readme file From 4f12343cd25f672426fc277e0e52cceffc1a1210 Mon Sep 17 00:00:00 2001 From: jiyuzhuang Date: Thu, 1 Aug 2024 14:33:01 +0200 Subject: [PATCH 18/46] =?UTF-8?q?=F0=9F=8E=A8=20(core):=20Improve=20code?= =?UTF-8?q?=20visibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/api/apdu/utils/ApduBuilder.ts | 33 ++++++++++--------- .../command/SignTransactionCommand.ts | 4 +-- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/core/src/api/apdu/utils/ApduBuilder.ts b/packages/core/src/api/apdu/utils/ApduBuilder.ts index 1db6ff2d7..9ab0f712b 100644 --- a/packages/core/src/api/apdu/utils/ApduBuilder.ts +++ b/packages/core/src/api/apdu/utils/ApduBuilder.ts @@ -63,14 +63,15 @@ export class ApduBuilder { * Build a new Apdu instance with the current state of the builder * @returns {Apdu} - Returns a new Apdu instance */ - build = () => new Apdu(this._cla, this._ins, this._p1, this.p2, this.data); + build = (): Apdu => + new Apdu(this._cla, this._ins, this._p1, this.p2, this.data); /** * Add a 8-bit unsigned integer to the data field (max value 0xff = 255) * @param value?: number - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - add8BitUIntToData = (value?: number) => { + add8BitUIntToData = (value?: number): ApduBuilder => { if (typeof value === "undefined" || isNaN(value)) { this.errors?.push(new InvalidValueError("byte", value?.toString())); return this; @@ -97,7 +98,7 @@ export class ApduBuilder { * @param value: number - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - add16BitUIntToData = (value: number) => { + add16BitUIntToData = (value: number): ApduBuilder => { if (value > MAX_16_BIT_UINT) { this.errors?.push( new ValueOverflowError(value.toString(), MAX_16_BIT_UINT), @@ -120,7 +121,7 @@ export class ApduBuilder { * @param value: number - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - add32BitUIntToData = (value: number) => { + add32BitUIntToData = (value: number): ApduBuilder => { if (value > MAX_32_BIT_UINT) { this.errors?.push( new ValueOverflowError(value.toString(), MAX_32_BIT_UINT), @@ -145,7 +146,7 @@ export class ApduBuilder { * @param value: Uint8Array - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - addBufferToData = (value: Uint8Array) => { + addBufferToData = (value: Uint8Array): ApduBuilder => { if (!this.hasEnoughLengthRemaining(value)) { this.errors?.push(new DataOverflowError(value.toString())); return this; @@ -163,7 +164,7 @@ export class ApduBuilder { * @param value: string - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - addHexaStringToData = (value: string) => { + addHexaStringToData = (value: string): ApduBuilder => { const result = this.getHexaString(value); if (!result.length) { this.errors?.push(new HexaStringEncodeError(value)); @@ -178,7 +179,7 @@ export class ApduBuilder { * @param value: string - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - addAsciiStringToData = (value: string) => { + addAsciiStringToData = (value: string): ApduBuilder => { let hexa = 0; if (!this.hasEnoughLengthRemaining(value)) { @@ -201,7 +202,7 @@ export class ApduBuilder { * @param value: string - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - encodeInLVFromHexa = (value: string) => { + encodeInLVFromHexa = (value: string): ApduBuilder => { const result: number[] = this.getHexaString(value); if (!result.length) { @@ -227,7 +228,7 @@ export class ApduBuilder { * @param value: Uint8Array - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - encodeInLVFromBuffer = (value: Uint8Array) => { + encodeInLVFromBuffer = (value: Uint8Array): ApduBuilder => { if (!this.hasEnoughLengthRemaining(value, true)) { this.errors?.push(new DataOverflowError(value.toString())); return this; @@ -245,7 +246,7 @@ export class ApduBuilder { * @param value: string - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - encodeInLVFromAscii = (value: string) => { + encodeInLVFromAscii = (value: string): ApduBuilder => { if (!this.hasEnoughLengthRemaining(value, true)) { this.errors?.push(new DataOverflowError(value)); return this; @@ -260,7 +261,7 @@ export class ApduBuilder { * Returns the remaining payload length * @returns {number} */ - getAvailablePayloadLength = () => { + getAvailablePayloadLength = (): number => { return APDU_MAX_SIZE - (HEADER_LENGTH + (this.data?.length ?? 0)); }; @@ -269,7 +270,7 @@ export class ApduBuilder { * @param value: string - The value to convert to hexadecimal * @returns {number[]} - Returns an array of numbers representing the hexadecimal value */ - getHexaString = (value: string) => { + getHexaString = (value: string): number[] => { const table: number[] = []; if (!value.length) return []; @@ -307,7 +308,7 @@ export class ApduBuilder { * Returns the current errors * @returns {AppBuilderError[]} - Returns an array of errors */ - getErrors = () => this.errors; + getErrors = (): AppBuilderError[] => this.errors; // =========== // Private API @@ -321,8 +322,8 @@ export class ApduBuilder { */ private hasEnoughLengthRemaining = ( value: string | Uint8Array | number[], - hasLv = false, - ) => { + hasLv: boolean = false, + ): boolean => { return ( HEADER_LENGTH + (this.data?.length ?? 0) + @@ -337,7 +338,7 @@ export class ApduBuilder { * @param value: number[] - The value to add to the data * @returns {ApduBuilder} - Returns the current instance of ApduBuilder */ - private addNumbers = (value: number[]) => { + private addNumbers = (value: number[]): ApduBuilder => { if (!this.hasEnoughLengthRemaining(value)) { this.errors?.push(new DataOverflowError(value.toString())); return this; diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts index c0113352b..c7b139758 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignTransactionCommand.ts @@ -10,9 +10,7 @@ import { HexaString, InvalidStatusWordError, } from "@ledgerhq/device-sdk-core"; -import { Just } from "purify-ts"; -import { Nothing } from "purify-ts"; -import { Maybe } from "purify-ts"; +import { Just, Maybe, Nothing } from "purify-ts"; import { DerivationPathUtils } from "@internal/shared/utils/DerivationPathUtils"; From 899d15152c2cf67b19cb6ca83dc1fbbd0e79ae27 Mon Sep 17 00:00:00 2001 From: jiyuzhuang Date: Thu, 1 Aug 2024 14:35:31 +0200 Subject: [PATCH 19/46] =?UTF-8?q?=E2=9C=A8=20(keyring-eth):=20Implement=20?= =?UTF-8?q?ProvideDomainNameCommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/happy-lions-eat.md | 5 ++ .changeset/proud-flies-type.md | 5 ++ .../command/ProvideDomainNameCommand.test.ts | 49 +++++++++++++++++ .../command/ProvideDomainNameCommand.ts | 55 ++++++++++++++++++- 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 .changeset/happy-lions-eat.md create mode 100644 .changeset/proud-flies-type.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts diff --git a/.changeset/happy-lions-eat.md b/.changeset/happy-lions-eat.md new file mode 100644 index 000000000..282f441d6 --- /dev/null +++ b/.changeset/happy-lions-eat.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-sdk-core": patch +--- + +Improve code visibility diff --git a/.changeset/proud-flies-type.md b/.changeset/proud-flies-type.md new file mode 100644 index 000000000..18ca07f8a --- /dev/null +++ b/.changeset/proud-flies-type.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement ProvideDomainNameCommand diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts new file mode 100644 index 000000000..bd62597b9 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts @@ -0,0 +1,49 @@ +import { + ApduResponse, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +import { + ProvideDomainNameCommand, + ProvideDomainNameCommandArgs, +} from "./ProvideDomainNameCommand"; + +const FIRST_CHUNK_APDU = Uint8Array.from([ + 0xe0, 0x22, 0x01, 0x00, 0x06, 0x4c, 0x65, 0x64, 0x67, 0x65, 0x72, +]); + +describe("ProvideDomainNameCommand", () => { + describe("getApdu", () => { + it("should return the raw APDU", () => { + const args: ProvideDomainNameCommandArgs = { + data: "4C6564676572", + index: 0, + }; + const command = new ProvideDomainNameCommand(args); + const apdu = command.getApdu(); + expect(apdu.getRawApdu()).toStrictEqual(FIRST_CHUNK_APDU); + }); + }); + + describe("parseResponse", () => { + it("should throw an error if the response status code is invalid", () => { + const response: ApduResponse = { + data: Buffer.from([]), + statusCode: Buffer.from([0x6a, 0x80]), // Invalid status code + }; + const command = new ProvideDomainNameCommand({ data: "", index: 0 }); + expect(() => command.parseResponse(response)).toThrow( + InvalidStatusWordError, + ); + }); + + it("should not throw if the response status code is correct", () => { + const response: ApduResponse = { + data: Buffer.from([]), + statusCode: Buffer.from([0x90, 0x00]), // Success status code + }; + const command = new ProvideDomainNameCommand({ data: "", index: 0 }); + expect(() => command.parseResponse(response)).not.toThrow(); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts index 08f4ffa79..671bd37ed 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts @@ -1,2 +1,55 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#provide-domain-name -export class ProvideDomainNameCommand {} +import { + Apdu, + ApduBuilder, + ApduParser, + ApduResponse, + type Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +export type ProvideDomainNameCommandArgs = { + /** + * The stringified hexa representation of the domain name. + * @example "4C6564676572" (hexa for "Ledger") + */ + data: string; + /** + * The index of the chunk. + */ + index: number; +}; + +/** + * The command that provides a chunk of the domain name to the device. + */ +export class ProvideDomainNameCommand implements Command { + constructor(private args: ProvideDomainNameCommandArgs) {} + + getApdu(): Apdu { + const apduBuilderArgs = { + cla: 0xe0, + ins: 0x22, + p1: this.args.index === 0 ? 0x01 : 0x00, + p2: 0x00, + }; + + return new ApduBuilder(apduBuilderArgs) + .addHexaStringToData(this.args.data) + .build(); + } + + parseResponse(response: ApduResponse): void { + const parser = new ApduParser(response); + + // TODO: handle the error correctly using a generic error handler + if (!CommandUtils.isSuccessResponse(response)) { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + response.statusCode, + )}`, + ); + } + } +} From 0ef06260b4cf87c3cb41fe2819e8efd849b2f336 Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Wed, 24 Jul 2024 15:30:51 +0200 Subject: [PATCH 20/46] =?UTF-8?q?=E2=9C=A8=20(core):=20Add=20ManagerApi=20?= =?UTF-8?q?service=20to=20core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/ninety-shirts-beg.md | 5 +++ packages/config/jest/jest-preset.js | 2 +- packages/core/jest.config.ts | 12 ++++-- packages/core/package.json | 1 + packages/core/src/api/DeviceSdk.ts | 4 +- packages/core/src/api/DeviceSdkBuilder.ts | 20 ++++++++- packages/core/src/api/SdkConfig.ts | 3 ++ .../use-case/SendCommandUseCase.test.ts | 16 ++++++- .../src/api/device-action/DeviceAction.ts | 2 + .../__test-utils__/makeInternalApi.ts | 18 ++++++++ .../GetDeviceStatusDeviceAction.test.ts | 43 ++++++++----------- .../GoToDashboardDeviceAction.test.ts | 43 ++++++++----------- .../os/ListApps/ListAppsDeviceAction.test.ts | 36 ++++++---------- .../OpenAppDeviceAction.test.ts | 41 +++++++----------- packages/core/src/di.ts | 9 +++- .../model/DeviceSession.stub.ts | 3 ++ .../device-session/model/DeviceSession.ts | 9 ++-- .../DefaultDeviceSessionService.test.ts | 13 ++++++ .../GetDeviceSessionStateUseCase.test.ts | 13 ++++++ .../discovery/di/discoveryModule.test.ts | 8 ++++ .../internal/discovery/di/discoveryModule.ts | 4 +- .../discovery/use-case/ConnectUseCase.test.ts | 26 ++++++++++- .../discovery/use-case/ConnectUseCase.ts | 7 +++ .../use-case/DisconnectUseCase.test.ts | 20 ++++++++- .../data/DefaultManagerApiDataSource.test.ts | 21 +++++++++ .../data/DefaultManagerApiDataSource.ts | 22 ++++++++++ .../manager-api/data/ManagerApiDataSource.ts | 5 +++ .../__mocks__/DefaultManagerApiDataSource.ts | 5 +++ .../manager-api/di/managerApiModule.test.ts | 41 ++++++++++++++++++ .../manager-api/di/managerApiModule.ts | 29 +++++++++++++ .../manager-api/di/managerApiTypes.ts | 5 +++ .../src/internal/manager-api/model/Const.ts | 1 + .../manager-api/model/ManagerApiResponses.ts | 34 +++++++++++++++ .../service/DefaultManagerApiService.test.ts | 21 +++++++++ .../service/DefaultManagerApiService.ts | 29 +++++++++++++ .../manager-api/service/ManagerApiService.ts | 6 +++ .../send/use-case/SendApduUseCase.test.ts | 19 +++++++- .../GetConnectedDeviceUseCase.test.ts | 14 ++++++ packages/signer/context-module/jest.config.ts | 11 +++-- packages/signer/keyring-eth/jest.config.ts | 12 ++++-- pnpm-lock.yaml | 3 ++ 41 files changed, 508 insertions(+), 128 deletions(-) create mode 100644 .changeset/ninety-shirts-beg.md create mode 100644 packages/core/src/api/SdkConfig.ts create mode 100644 packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts create mode 100644 packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts create mode 100644 packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts create mode 100644 packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts create mode 100644 packages/core/src/internal/manager-api/data/__mocks__/DefaultManagerApiDataSource.ts create mode 100644 packages/core/src/internal/manager-api/di/managerApiModule.test.ts create mode 100644 packages/core/src/internal/manager-api/di/managerApiModule.ts create mode 100644 packages/core/src/internal/manager-api/di/managerApiTypes.ts create mode 100644 packages/core/src/internal/manager-api/model/Const.ts create mode 100644 packages/core/src/internal/manager-api/model/ManagerApiResponses.ts create mode 100644 packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts create mode 100644 packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts create mode 100644 packages/core/src/internal/manager-api/service/ManagerApiService.ts diff --git a/.changeset/ninety-shirts-beg.md b/.changeset/ninety-shirts-beg.md new file mode 100644 index 000000000..d27e80481 --- /dev/null +++ b/.changeset/ninety-shirts-beg.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-sdk-core": minor +--- + +Add ManagerApi service to core diff --git a/packages/config/jest/jest-preset.js b/packages/config/jest/jest-preset.js index 50b8f2def..157fbdd2f 100644 --- a/packages/config/jest/jest-preset.js +++ b/packages/config/jest/jest-preset.js @@ -1,6 +1,6 @@ /** @type {import('jest').Config} */ const config = { - preset: "ts-jest", + preset: "ts-jest/presets/js-with-ts", transform: { "^.+\\.ts$": "ts-jest", }, diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts index ac318d966..16fcf65c3 100644 --- a/packages/core/jest.config.ts +++ b/packages/core/jest.config.ts @@ -1,5 +1,11 @@ /* eslint no-restricted-syntax: 0 */ -import type { JestConfigWithTsJest } from "ts-jest"; +import { JestConfigWithTsJest, pathsToModuleNameMapper } from "ts-jest"; + +import { compilerOptions } from "./tsconfig.json"; + +const internalPaths = pathsToModuleNameMapper(compilerOptions.paths, { + prefix: "/", +}); const config: JestConfigWithTsJest = { preset: "@ledgerhq/jest-config-dsdk", @@ -12,9 +18,7 @@ const config: JestConfigWithTsJest = { "!src/api/index.ts", ], moduleNameMapper: { - "^@api/(.*)$": "/src/api/$1", - "^@internal/(.*)$": "/src/internal/$1", - "^@root/(.*)$": "/$1", + ...internalPaths, }, }; diff --git a/packages/core/package.json b/packages/core/package.json index ceb1a9b6b..95a46f1e8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "@sentry/minimal": "^6.19.7", + "axios": "^1.7.2", "inversify": "^6.0.2", "inversify-logger-middleware": "^3.1.0", "purify-ts": "^2.1.0", diff --git a/packages/core/src/api/DeviceSdk.ts b/packages/core/src/api/DeviceSdk.ts index 0216685ae..be75cc3df 100644 --- a/packages/core/src/api/DeviceSdk.ts +++ b/packages/core/src/api/DeviceSdk.ts @@ -53,11 +53,11 @@ import { SdkError } from "./Error"; export class DeviceSdk { readonly container: Container; /** @internal */ - constructor({ stub, loggers }: Partial = {}) { + constructor({ stub, loggers, config }: Partial = {}) { // NOTE: MakeContainerProps might not be the exact type here // For the init of the project this is sufficient, but we might need to // update the constructor arguments as we go (we might have more than just the container config) - this.container = makeContainer({ stub, loggers }); + this.container = makeContainer({ stub, loggers, config }); } /** diff --git a/packages/core/src/api/DeviceSdkBuilder.ts b/packages/core/src/api/DeviceSdkBuilder.ts index c169e3c80..b2589ca11 100644 --- a/packages/core/src/api/DeviceSdkBuilder.ts +++ b/packages/core/src/api/DeviceSdkBuilder.ts @@ -1,5 +1,8 @@ +import { MANAGER_API_BASE_URL } from "@internal/manager-api/model/Const"; + import { LoggerSubscriberService } from "./logger-subscriber/service/LoggerSubscriberService"; import { DeviceSdk } from "./DeviceSdk"; +import { SdkConfig } from "./SdkConfig"; /** * Builder for the `DeviceSdk` class. @@ -15,9 +18,16 @@ import { DeviceSdk } from "./DeviceSdk"; export class LedgerDeviceSdkBuilder { private stub = false; private readonly loggers: LoggerSubscriberService[] = []; + private config: SdkConfig = { + managerApiUrl: MANAGER_API_BASE_URL, + }; build(): DeviceSdk { - return new DeviceSdk({ stub: this.stub, loggers: this.loggers }); + return new DeviceSdk({ + stub: this.stub, + loggers: this.loggers, + config: this.config, + }); } setStub(stubbed: boolean): LedgerDeviceSdkBuilder { @@ -32,4 +42,12 @@ export class LedgerDeviceSdkBuilder { this.loggers.push(logger); return this; } + + addConfig(config: SdkConfig): LedgerDeviceSdkBuilder { + this.config = { + ...this.config, + ...config, + }; + return this; + } } diff --git a/packages/core/src/api/SdkConfig.ts b/packages/core/src/api/SdkConfig.ts new file mode 100644 index 000000000..a910a5f2b --- /dev/null +++ b/packages/core/src/api/SdkConfig.ts @@ -0,0 +1,3 @@ +export type SdkConfig = { + managerApiUrl: string; +}; diff --git a/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts b/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts index 7822a416a..13ac2c7cb 100644 --- a/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts +++ b/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts @@ -6,11 +6,17 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { SendCommandUseCase } from "./SendCommandUseCase"; let logger: LoggerPublisherService; let sessionService: DeviceSessionService; +let managerApi: ManagerApiService; +let managerApiDataSource: ManagerApiDataSource; const fakeSessionId = "fakeSessionId"; let command: Command<{ status: string }>; @@ -18,6 +24,10 @@ describe("SendCommandUseCase", () => { beforeEach(() => { logger = new DefaultLoggerPublisherService([], "send-command-use-case"); sessionService = new DefaultDeviceSessionService(() => logger); + managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiUrl: "http://fake.url", + }); + managerApi = new DefaultManagerApiService(managerApiDataSource); command = { getApdu: jest.fn(), parseResponse: jest.fn(), @@ -29,7 +39,11 @@ describe("SendCommandUseCase", () => { }); it("should send a command to a connected device", async () => { - const deviceSession = deviceSessionStubBuilder({}, () => logger); + const deviceSession = deviceSessionStubBuilder( + {}, + () => logger, + managerApi, + ); sessionService.addDeviceSession(deviceSession); const useCase = new SendCommandUseCase(sessionService, () => logger); diff --git a/packages/core/src/api/device-action/DeviceAction.ts b/packages/core/src/api/device-action/DeviceAction.ts index ef4b67989..4b6c9e438 100644 --- a/packages/core/src/api/device-action/DeviceAction.ts +++ b/packages/core/src/api/device-action/DeviceAction.ts @@ -3,6 +3,7 @@ import { Observable } from "rxjs"; import { Command } from "@api/command/Command"; import { DeviceSessionState } from "@api/device-session/DeviceSessionState"; import { SdkError } from "@api/Error"; +import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { DeviceActionState } from "./model/DeviceActionState"; @@ -15,6 +16,7 @@ export type InternalApi = { readonly setDeviceSessionState: ( state: DeviceSessionState, ) => DeviceSessionState; + managerApiService: ManagerApiService; }; export type DeviceActionIntermediateValue = { diff --git a/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts b/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts new file mode 100644 index 000000000..70125fe31 --- /dev/null +++ b/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts @@ -0,0 +1,18 @@ +import { InternalApi } from "@api/device-action/DeviceAction"; +import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; + +const sendCommandMock = jest.fn(); +const apiGetDeviceSessionStateMock = jest.fn(); +const apiGetDeviceSessionStateObservableMock = jest.fn(); +const setDeviceSessionStateMock = jest.fn(); +const managerApiServiceMock = jest.fn() as unknown as ManagerApiService; + +export function makeInternalApiMock(): jest.Mocked { + return { + sendCommand: sendCommandMock, + getDeviceSessionState: apiGetDeviceSessionStateMock, + getDeviceSessionStateObservable: apiGetDeviceSessionStateObservableMock, + setDeviceSessionState: setDeviceSessionStateMock, + managerApiService: managerApiServiceMock, + }; +} diff --git a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts index 0a2c0d11b..f7c2ba13a 100644 --- a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts @@ -1,8 +1,8 @@ import { interval, Observable } from "rxjs"; import { DeviceStatus } from "@api/device/DeviceStatus"; +import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; -import { InternalApi } from "@api/device-action/DeviceAction"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; import { @@ -32,20 +32,11 @@ describe("GetDeviceStatusDeviceAction", () => { }; } - const sendCommandMock = jest.fn(); - const apiGetDeviceSessionStateMock = jest.fn(); - const apiGetDeviceSessionStateObservableMock = jest.fn(); - const setDeviceSessionStateMock = jest.fn(); - - function internalApiMock(): InternalApi { - return { - sendCommand: sendCommandMock, - getDeviceSessionState: apiGetDeviceSessionStateMock, - getDeviceSessionStateObservable: apiGetDeviceSessionStateObservableMock, - setDeviceSessionState: setDeviceSessionStateMock, - }; - } - + const { + sendCommand: sendCommandMock, + getDeviceSessionState: apiGetDeviceSessionStateMock, + getDeviceSessionStateObservable: apiGetDeviceSessionStateObservableMock, + } = makeInternalApiMock(); beforeEach(() => { jest.resetAllMocks(); isDeviceOnboardedMock.mockReturnValue(true); @@ -58,7 +49,7 @@ describe("GetDeviceStatusDeviceAction", () => { }); apiGetDeviceSessionStateMock.mockReturnValue({ - sessionStateType: DeviceSessionStateType.Connected, + sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "mockedCurrentApp", }); @@ -93,7 +84,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -175,7 +166,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -227,7 +218,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -313,7 +304,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -346,7 +337,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -402,7 +393,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -478,7 +469,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -561,7 +552,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -569,7 +560,7 @@ describe("GetDeviceStatusDeviceAction", () => { it("should emit a stopped state if the action is cancelled", (done) => { apiGetDeviceSessionStateMock.mockReturnValue({ - sessionStateType: DeviceSessionStateType.Connected, + sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "mockedCurrentApp", }); @@ -598,7 +589,7 @@ describe("GetDeviceStatusDeviceAction", () => { const { cancel } = testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); cancel(); diff --git a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts b/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts index ad1bf2191..efaa33213 100644 --- a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts @@ -2,8 +2,8 @@ import { Left, Right } from "purify-ts"; import { assign, createMachine } from "xstate"; import { DeviceStatus } from "@api/device/DeviceStatus"; +import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; -import { InternalApi } from "@api/device-action/DeviceAction"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; import { UnknownDAError } from "@api/device-action/os/Errors"; @@ -65,19 +65,10 @@ describe("GoToDashboardDeviceAction", () => { }; } - const sendCommandMock = jest.fn(); - const apiGetDeviceSessionStateMock = jest.fn(); - const apiGetDeviceSessionStateObservableMock = jest.fn(); - const setDeviceSessionStateMock = jest.fn(); - - function internalApiMock(): InternalApi { - return { - sendCommand: sendCommandMock, - getDeviceSessionState: apiGetDeviceSessionStateMock, - getDeviceSessionStateObservable: apiGetDeviceSessionStateObservableMock, - setDeviceSessionState: setDeviceSessionStateMock, - }; - } + const { + sendCommand: sendCommandMock, + getDeviceSessionState: apiGetDeviceSessionStateMock, + } = makeInternalApiMock(); beforeEach(() => { jest.resetAllMocks(); @@ -92,7 +83,7 @@ describe("GoToDashboardDeviceAction", () => { }); apiGetDeviceSessionStateMock.mockReturnValue({ - sessionStateType: DeviceSessionStateType.Connected, + sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "BOLOS", }); @@ -125,7 +116,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -141,7 +132,7 @@ describe("GoToDashboardDeviceAction", () => { }); apiGetDeviceSessionStateMock.mockReturnValue({ - sessionStateType: DeviceSessionStateType.Connected, + sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "Bitcoin", }); @@ -191,7 +182,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -248,7 +239,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -303,7 +294,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -318,7 +309,7 @@ describe("GoToDashboardDeviceAction", () => { }); apiGetDeviceSessionStateMock.mockReturnValue({ - sessionStateType: DeviceSessionStateType.Connected, + sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "BOLOS", }); @@ -345,7 +336,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -401,7 +392,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -465,7 +456,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -530,7 +521,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -605,7 +596,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); diff --git a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts b/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts index 478e282b1..2250c38c4 100644 --- a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts @@ -1,8 +1,8 @@ import { Left, Right } from "purify-ts"; import { assign, createMachine } from "xstate"; +import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; -import { InternalApi } from "@api/device-action/DeviceAction"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; import { @@ -96,19 +96,7 @@ const setupGoToDashboardMock = (error: boolean = false) => { }; describe("ListAppsDeviceAction", () => { - const sendCommandMock = jest.fn(); - const apiGetDeviceSessionStateMock = jest.fn(); - const apiGetDeviceSessionStateObservableMock = jest.fn(); - const setDeviceSessionStateMock = jest.fn(); - - function internalApiMock(): InternalApi { - return { - sendCommand: sendCommandMock, - getDeviceSessionState: apiGetDeviceSessionStateMock, - getDeviceSessionStateObservable: apiGetDeviceSessionStateObservableMock, - setDeviceSessionState: setDeviceSessionStateMock, - }; - } + const { sendCommand: sendCommandMock } = makeInternalApiMock(); beforeEach(() => { jest.resetAllMocks(); @@ -151,7 +139,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -192,7 +180,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -241,7 +229,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -290,7 +278,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -346,7 +334,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -408,7 +396,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -445,7 +433,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -480,7 +468,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -521,7 +509,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -570,7 +558,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); diff --git a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts b/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts index 2712ef944..6ebfc8852 100644 --- a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts @@ -1,7 +1,7 @@ import { InvalidStatusWordError } from "@api/command/Errors"; import { DeviceStatus } from "@api/device/DeviceStatus"; +import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; -import { InternalApi } from "@api/device-action/DeviceAction"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; import { @@ -30,19 +30,10 @@ describe("OpenAppDeviceAction", () => { }; } - const sendCommandMock = jest.fn(); - const apiGetDeviceSessionStateMock = jest.fn(); - const apiGetDeviceSessionStateObservableMock = jest.fn(); - const setDeviceSessionStateMock = jest.fn(); - - function internalApiMock(): InternalApi { - return { - sendCommand: sendCommandMock, - getDeviceSessionState: apiGetDeviceSessionStateMock, - getDeviceSessionStateObservable: apiGetDeviceSessionStateObservableMock, - setDeviceSessionState: setDeviceSessionStateMock, - }; - } + const { + sendCommand: sendCommandMock, + getDeviceSessionState: apiGetDeviceSessionStateMock, + } = makeInternalApiMock(); beforeEach(() => { jest.resetAllMocks(); @@ -82,7 +73,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -123,7 +114,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -169,7 +160,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -221,7 +212,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -254,7 +245,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -284,7 +275,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -324,7 +315,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -369,7 +360,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -417,7 +408,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -470,7 +461,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -509,7 +500,7 @@ describe("OpenAppDeviceAction", () => { const { cancel } = testDeviceActionStates( openAppDeviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); cancel(); diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index a5516674d..2b37f3447 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -1,15 +1,17 @@ import { Container } from "inversify"; +// Uncomment this line to enable the logger middleware +// import { makeLoggerMiddleware } from "inversify-logger-middleware"; import { commandModuleFactory } from "@api/command/di/commandModule"; import { deviceActionModuleFactory } from "@api/device-action/di/deviceActionModule"; import { LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService"; -// Uncomment this line to enable the logger middleware -// import { makeLoggerMiddleware } from "inversify-logger-middleware"; +import { SdkConfig } from "@api/SdkConfig"; import { configModuleFactory } from "@internal/config/di/configModule"; import { deviceModelModuleFactory } from "@internal/device-model/di/deviceModelModule"; import { deviceSessionModuleFactory } from "@internal/device-session/di/deviceSessionModule"; import { discoveryModuleFactory } from "@internal/discovery/di/discoveryModule"; import { loggerModuleFactory } from "@internal/logger-publisher/di/loggerModule"; +import { managerApiModuleFactory } from "@internal/manager-api/di/managerApiModule"; import { sendModuleFactory } from "@internal/send/di/sendModule"; import { usbModuleFactory } from "@internal/usb/di/usbModule"; @@ -19,11 +21,13 @@ import { usbModuleFactory } from "@internal/usb/di/usbModule"; export type MakeContainerProps = { stub: boolean; loggers: LoggerSubscriberService[]; + config: SdkConfig; }; export const makeContainer = ({ stub = false, loggers = [], + config, }: Partial) => { const container = new Container(); @@ -34,6 +38,7 @@ export const makeContainer = ({ configModuleFactory({ stub }), deviceModelModuleFactory({ stub }), usbModuleFactory({ stub }), + managerApiModuleFactory({ stub, config }), discoveryModuleFactory({ stub }), loggerModuleFactory({ subscribers: loggers }), deviceSessionModuleFactory({ stub }), diff --git a/packages/core/src/internal/device-session/model/DeviceSession.stub.ts b/packages/core/src/internal/device-session/model/DeviceSession.stub.ts index bc9f111a9..287744c8e 100644 --- a/packages/core/src/internal/device-session/model/DeviceSession.stub.ts +++ b/packages/core/src/internal/device-session/model/DeviceSession.stub.ts @@ -3,11 +3,13 @@ import { SessionConstructorArgs, } from "@internal/device-session/model/DeviceSession"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import type { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub"; export const deviceSessionStubBuilder = ( props: Partial = {}, loggerFactory: (tag: string) => LoggerPublisherService, + managerApi: ManagerApiService, ) => new DeviceSession( { @@ -16,4 +18,5 @@ export const deviceSessionStubBuilder = ( ...props, }, loggerFactory, + managerApi, ); diff --git a/packages/core/src/internal/device-session/model/DeviceSession.ts b/packages/core/src/internal/device-session/model/DeviceSession.ts index f8fb2a607..fc08f6594 100644 --- a/packages/core/src/internal/device-session/model/DeviceSession.ts +++ b/packages/core/src/internal/device-session/model/DeviceSession.ts @@ -1,4 +1,4 @@ -import { inject } from "inversify"; +// import { inject } from "inversify"; import { BehaviorSubject } from "rxjs"; import { v4 as uuidv4 } from "uuid"; @@ -16,8 +16,8 @@ import { } from "@api/device-session/DeviceSessionState"; import { DeviceSessionId } from "@api/device-session/types"; import { SdkError } from "@api/Error"; -import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice"; import { DeviceSessionRefresher } from "./DeviceSessionRefresher"; @@ -35,11 +35,12 @@ export class DeviceSession { private readonly _connectedDevice: InternalConnectedDevice; private readonly _deviceState: BehaviorSubject; private readonly _refresher: DeviceSessionRefresher; + private readonly _managerApiService: ManagerApiService; constructor( { connectedDevice, id = uuidv4() }: SessionConstructorArgs, - @inject(loggerTypes.LoggerPublisherServiceFactory) loggerModuleFactory: (tag: string) => LoggerPublisherService, + managerApiService: ManagerApiService, ) { this._id = id; this._connectedDevice = connectedDevice; @@ -61,6 +62,7 @@ export class DeviceSession { }, loggerModuleFactory("device-session-refresher"), ); + this._managerApiService = managerApiService; } public get id() { @@ -146,6 +148,7 @@ export class DeviceSession { this.setDeviceSessionState(state); return this._deviceState.getValue(); }, + managerApiService: this._managerApiService, }); return { diff --git a/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts b/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts index d3b1b380d..91734c37b 100644 --- a/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts +++ b/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts @@ -3,25 +3,38 @@ import { Either, Left } from "purify-ts"; import { DeviceSession } from "@internal/device-session/model/DeviceSession"; import { DeviceSessionNotFound } from "@internal/device-session/model/Errors"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import type { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub"; import { DefaultDeviceSessionService } from "./DefaultDeviceSessionService"; jest.mock("@internal/logger-publisher/service/DefaultLoggerPublisherService"); +jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); let sessionService: DefaultDeviceSessionService; let loggerService: DefaultLoggerPublisherService; let deviceSession: DeviceSession; +let managerApi: ManagerApiService; +let managerApiDataSource: ManagerApiDataSource; describe("DefaultDeviceSessionService", () => { beforeEach(() => { jest.restoreAllMocks(); loggerService = new DefaultLoggerPublisherService([], "deviceSession"); sessionService = new DefaultDeviceSessionService(() => loggerService); + managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiUrl: "http://fake.url", + }); + managerApi = new DefaultManagerApiService(managerApiDataSource); + deviceSession = new DeviceSession( { connectedDevice: connectedDeviceStubBuilder(), }, () => loggerService, + managerApi, ); }); diff --git a/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts b/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts index bbaa1423e..ab07c305c 100644 --- a/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts +++ b/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts @@ -3,11 +3,19 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { GetDeviceSessionStateUseCase } from "./GetDeviceSessionStateUseCase"; +jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); + let logger: LoggerPublisherService; let sessionService: DeviceSessionService; +let managerApiDataSource: ManagerApiDataSource; +let managerApi: ManagerApiService; const fakeSessionId = "fakeSessionId"; @@ -17,6 +25,10 @@ describe("GetDeviceSessionStateUseCase", () => { [], "get-connected-device-use-case-test", ); + managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiUrl: "http://fake.url", + }); + managerApi = new DefaultManagerApiService(managerApiDataSource); sessionService = new DefaultDeviceSessionService(() => logger); }); @@ -25,6 +37,7 @@ describe("GetDeviceSessionStateUseCase", () => { const deviceSession = deviceSessionStubBuilder( { id: fakeSessionId }, () => logger, + managerApi, ); sessionService.addDeviceSession(deviceSession); const useCase = new GetDeviceSessionStateUseCase( diff --git a/packages/core/src/internal/discovery/di/discoveryModule.test.ts b/packages/core/src/internal/discovery/di/discoveryModule.test.ts index 010f99d91..500ed6c21 100644 --- a/packages/core/src/internal/discovery/di/discoveryModule.test.ts +++ b/packages/core/src/internal/discovery/di/discoveryModule.test.ts @@ -3,9 +3,11 @@ import { Container } from "inversify"; import { deviceModelModuleFactory } from "@internal/device-model/di/deviceModelModule"; import { deviceSessionModuleFactory } from "@internal/device-session/di/deviceSessionModule"; import { ConnectUseCase } from "@internal/discovery/use-case/ConnectUseCase"; +import { DisconnectUseCase } from "@internal/discovery/use-case/DisconnectUseCase"; import { StartDiscoveringUseCase } from "@internal/discovery/use-case/StartDiscoveringUseCase"; import { StopDiscoveringUseCase } from "@internal/discovery/use-case/StopDiscoveringUseCase"; import { loggerModuleFactory } from "@internal/logger-publisher/di/loggerModule"; +import { managerApiModuleFactory } from "@internal/manager-api/di/managerApiModule"; import { usbModuleFactory } from "@internal/usb/di/usbModule"; import { discoveryModuleFactory } from "./discoveryModule"; @@ -24,6 +26,9 @@ describe("discoveryModuleFactory", () => { usbModuleFactory(), deviceModelModuleFactory(), deviceSessionModuleFactory(), + managerApiModuleFactory({ + config: { managerApiUrl: "http://fake.url" }, + }), ); }); @@ -42,6 +47,9 @@ describe("discoveryModuleFactory", () => { ); expect(stopDiscoveringUseCase).toBeInstanceOf(StopDiscoveringUseCase); + const disconnectUseCase = container.get(discoveryTypes.DisconnectUseCase); + expect(disconnectUseCase).toBeInstanceOf(DisconnectUseCase); + const connectUseCase = container.get(discoveryTypes.ConnectUseCase); expect(connectUseCase).toBeInstanceOf(ConnectUseCase); }); diff --git a/packages/core/src/internal/discovery/di/discoveryModule.ts b/packages/core/src/internal/discovery/di/discoveryModule.ts index d2d80bd8b..97d7bf25a 100644 --- a/packages/core/src/internal/discovery/di/discoveryModule.ts +++ b/packages/core/src/internal/discovery/di/discoveryModule.ts @@ -16,10 +16,10 @@ export const discoveryModuleFactory = ({ stub = false, }: Partial = {}) => new ContainerModule((bind, _unbind, _isBound, rebind) => { - bind(discoveryTypes.StartDiscoveringUseCase).to(StartDiscoveringUseCase); - bind(discoveryTypes.StopDiscoveringUseCase).to(StopDiscoveringUseCase); bind(discoveryTypes.ConnectUseCase).to(ConnectUseCase); bind(discoveryTypes.DisconnectUseCase).to(DisconnectUseCase); + bind(discoveryTypes.StartDiscoveringUseCase).to(StartDiscoveringUseCase); + bind(discoveryTypes.StopDiscoveringUseCase).to(StopDiscoveringUseCase); if (stub) { rebind(discoveryTypes.StartDiscoveringUseCase).to(StubUseCase); diff --git a/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts b/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts index ef6952f4c..937240599 100644 --- a/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts +++ b/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts @@ -7,6 +7,10 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { UnknownDeviceError } from "@internal/usb/model/Errors"; import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub"; import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/usb/service/UsbHidDeviceConnectionFactory.stub"; @@ -14,9 +18,13 @@ import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport"; import { ConnectUseCase } from "./ConnectUseCase"; +jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); + let transport: WebUsbHidTransport; let logger: LoggerPublisherService; let sessionService: DeviceSessionService; +let managerApi: ManagerApiService; +let managerApiDataSource: ManagerApiDataSource; const fakeSessionId = "fakeSessionId"; describe("ConnectUseCase", () => { @@ -32,6 +40,10 @@ describe("ConnectUseCase", () => { usbHidDeviceConnectionFactoryStubBuilder(), ); sessionService = new DefaultDeviceSessionService(() => logger); + managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiUrl: "http://fake.url", + }); + managerApi = new DefaultManagerApiService(managerApiDataSource); }); afterAll(() => { @@ -43,7 +55,12 @@ describe("ConnectUseCase", () => { .spyOn(transport, "connect") .mockResolvedValue(Left(new UnknownDeviceError())); - const usecase = new ConnectUseCase(transport, sessionService, () => logger); + const usecase = new ConnectUseCase( + transport, + sessionService, + () => logger, + managerApi, + ); await expect(usecase.execute({ deviceId: "" })).rejects.toBeInstanceOf( UnknownDeviceError, @@ -55,7 +72,12 @@ describe("ConnectUseCase", () => { .spyOn(transport, "connect") .mockResolvedValue(Promise.resolve(Right(stubConnectedDevice))); - const usecase = new ConnectUseCase(transport, sessionService, () => logger); + const usecase = new ConnectUseCase( + transport, + sessionService, + () => logger, + managerApi, + ); const sessionId = await usecase.execute({ deviceId: "" }); expect(sessionId).toBe(fakeSessionId); diff --git a/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts b/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts index 853c60032..f460aac5d 100644 --- a/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts +++ b/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts @@ -7,6 +7,8 @@ import { DeviceSession } from "@internal/device-session/model/DeviceSession"; import type { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes"; +import type { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { usbDiTypes } from "@internal/usb/di/usbDiTypes"; import type { UsbHidTransport } from "@internal/usb/transport/UsbHidTransport"; @@ -28,6 +30,7 @@ export class ConnectUseCase { private readonly _usbHidTransport: UsbHidTransport; private readonly _sessionService: DeviceSessionService; private readonly _loggerFactory: (tag: string) => LoggerPublisherService; + private readonly _managerApi: ManagerApiService; private readonly _logger: LoggerPublisherService; constructor( @@ -37,11 +40,14 @@ export class ConnectUseCase { sessionService: DeviceSessionService, @inject(loggerTypes.LoggerPublisherServiceFactory) loggerFactory: (tag: string) => LoggerPublisherService, + @inject(managerApiTypes.ManagerApiService) + managerApi: ManagerApiService, ) { this._sessionService = sessionService; this._usbHidTransport = usbHidTransport; this._loggerFactory = loggerFactory; this._logger = loggerFactory("ConnectUseCase"); + this._managerApi = managerApi; } private handleDeviceDisconnect(deviceId: DeviceId) { @@ -69,6 +75,7 @@ export class ConnectUseCase { const deviceSession = new DeviceSession( { connectedDevice }, this._loggerFactory, + this._managerApi, ); this._sessionService.addDeviceSession(deviceSession); return deviceSession.id; diff --git a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts b/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts index 42e3aceda..d516719f9 100644 --- a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts +++ b/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts @@ -5,6 +5,10 @@ import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceS import { DeviceSessionNotFound } from "@internal/device-session/model/Errors"; import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { DisconnectError } from "@internal/usb/model/Errors"; import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub"; import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/usb/service/UsbHidDeviceConnectionFactory.stub"; @@ -20,6 +24,9 @@ const loggerFactory = jest new DefaultLoggerPublisherService([], "DisconnectUseCaseTest"), ); +let managerApi: ManagerApiService; +let managerApiDataSource: ManagerApiDataSource; + const sessionId = "sessionId"; describe("DisconnectUseCase", () => { @@ -35,12 +42,17 @@ describe("DisconnectUseCase", () => { it("should disconnect from a device", async () => { // Given const connectedDevice = connectedDeviceStubBuilder(); + managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiUrl: "http://fake.url", + }); + managerApi = new DefaultManagerApiService(managerApiDataSource); const deviceSession = deviceSessionStubBuilder( { id: sessionId, connectedDevice, }, loggerFactory, + managerApi, ); jest .spyOn(sessionService, "getDeviceSessionById") @@ -87,7 +99,13 @@ describe("DisconnectUseCase", () => { jest .spyOn(sessionService, "getDeviceSessionById") .mockImplementation(() => - Right(deviceSessionStubBuilder({ id: sessionId }, loggerFactory)), + Right( + deviceSessionStubBuilder( + { id: sessionId }, + loggerFactory, + managerApi, + ), + ), ); jest .spyOn(usbHidTransport, "disconnect") diff --git a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts new file mode 100644 index 000000000..e17f3ba83 --- /dev/null +++ b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts @@ -0,0 +1,21 @@ +import { MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const"; + +import { DefaultManagerApiDataSource } from "./DefaultManagerApiDataSource"; + +describe("DefaultManagerApiDataSource", () => { + it("fetch data", async () => { + const api = new DefaultManagerApiDataSource({ + managerApiUrl: MANAGER_API_BASE_URL, + }); + + // appFullHash from the ListApps/ListAppsContinue command's response + const hashes = [ + "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", + ]; + + const apps = await api.getAppsByHash(hashes); + // console.log(apps); + + expect(apps).toHaveLength(1); + }); +}); diff --git a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts new file mode 100644 index 000000000..317c9d69e --- /dev/null +++ b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts @@ -0,0 +1,22 @@ +import axios from "axios"; +import { inject, injectable } from "inversify"; + +import { type SdkConfig } from "@api/SdkConfig"; +import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes"; +import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; + +import { ManagerApiDataSource } from "./ManagerApiDataSource"; + +@injectable() +export class DefaultManagerApiDataSource implements ManagerApiDataSource { + private readonly baseUrl: string; + constructor(@inject(managerApiTypes.SdkConfig) config: SdkConfig) { + this.baseUrl = config.managerApiUrl; + } + + getAppsByHash(hashes: string[]) { + return axios + .post(`${this.baseUrl}/v2/apps/hash`, hashes) + .then((res) => res.data); + } +} diff --git a/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts new file mode 100644 index 000000000..58b9e5021 --- /dev/null +++ b/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts @@ -0,0 +1,5 @@ +import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; + +export interface ManagerApiDataSource { + getAppsByHash(hashes: string[]): Promise; +} diff --git a/packages/core/src/internal/manager-api/data/__mocks__/DefaultManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/__mocks__/DefaultManagerApiDataSource.ts new file mode 100644 index 000000000..2a56ef6b6 --- /dev/null +++ b/packages/core/src/internal/manager-api/data/__mocks__/DefaultManagerApiDataSource.ts @@ -0,0 +1,5 @@ +import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; + +export class DefaultManagerApiDataSource implements ManagerApiDataSource { + getAppsByHash = jest.fn(); +} diff --git a/packages/core/src/internal/manager-api/di/managerApiModule.test.ts b/packages/core/src/internal/manager-api/di/managerApiModule.test.ts new file mode 100644 index 000000000..ea46b3160 --- /dev/null +++ b/packages/core/src/internal/manager-api/di/managerApiModule.test.ts @@ -0,0 +1,41 @@ +import { Container } from "inversify"; + +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; + +import { managerApiModuleFactory } from "./managerApiModule"; +import { managerApiTypes } from "./managerApiTypes"; +// import { types } from "./managerApiTypes"; + +describe("managerApiModuleFactory", () => { + describe("Default", () => { + let container: Container; + let mod: ReturnType; + beforeEach(() => { + mod = managerApiModuleFactory({ + config: { managerApiUrl: "http://fake.url" }, + }); + container = new Container(); + container.load(mod); + }); + + it("should return the config module", () => { + expect(mod).toBeDefined(); + }); + + it("should return none mocked use cases", () => { + const managerApiDataSource = container.get( + managerApiTypes.ManagerApiDataSource, + ); + expect(managerApiDataSource).toBeInstanceOf(DefaultManagerApiDataSource); + + const managerApiService = container.get( + managerApiTypes.ManagerApiService, + ); + expect(managerApiService).toBeInstanceOf(DefaultManagerApiService); + + const config = container.get(managerApiTypes.SdkConfig); + expect(config).toEqual({ managerApiUrl: "http://fake.url" }); + }); + }); +}); diff --git a/packages/core/src/internal/manager-api/di/managerApiModule.ts b/packages/core/src/internal/manager-api/di/managerApiModule.ts new file mode 100644 index 000000000..61e10d1da --- /dev/null +++ b/packages/core/src/internal/manager-api/di/managerApiModule.ts @@ -0,0 +1,29 @@ +import { ContainerModule } from "inversify"; + +import { SdkConfig } from "@api/SdkConfig"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { StubUseCase } from "@root/src/di.stub"; + +import { managerApiTypes } from "./managerApiTypes"; + +type FactoryProps = { + stub: boolean; + config: SdkConfig; +}; + +export const managerApiModuleFactory = ({ + stub = false, + config, +}: Partial = {}) => + new ContainerModule((bind, _unbind, _isBound, rebind) => { + bind(managerApiTypes.SdkConfig).toConstantValue(config); + + bind(managerApiTypes.ManagerApiDataSource).to(DefaultManagerApiDataSource); + bind(managerApiTypes.ManagerApiService).to(DefaultManagerApiService); + + if (stub) { + rebind(managerApiTypes.ManagerApiDataSource).to(StubUseCase); + rebind(managerApiTypes.ManagerApiService).to(StubUseCase); + } + }); diff --git a/packages/core/src/internal/manager-api/di/managerApiTypes.ts b/packages/core/src/internal/manager-api/di/managerApiTypes.ts new file mode 100644 index 000000000..c2a68cc2c --- /dev/null +++ b/packages/core/src/internal/manager-api/di/managerApiTypes.ts @@ -0,0 +1,5 @@ +export const managerApiTypes = { + ManagerApiService: Symbol.for("ManagerApiService"), + ManagerApiDataSource: Symbol.for("ManagerApiDataSource"), + SdkConfig: Symbol.for("SdkConfig"), +}; diff --git a/packages/core/src/internal/manager-api/model/Const.ts b/packages/core/src/internal/manager-api/model/Const.ts new file mode 100644 index 000000000..8b8dbbb1f --- /dev/null +++ b/packages/core/src/internal/manager-api/model/Const.ts @@ -0,0 +1 @@ +export const MANAGER_API_BASE_URL = "https://manager.api.live.ledger.com/api"; diff --git a/packages/core/src/internal/manager-api/model/ManagerApiResponses.ts b/packages/core/src/internal/manager-api/model/ManagerApiResponses.ts new file mode 100644 index 000000000..40ee6b0b2 --- /dev/null +++ b/packages/core/src/internal/manager-api/model/ManagerApiResponses.ts @@ -0,0 +1,34 @@ +export type Id = number; + +export enum AppType { + currency = "currency", + plugin = "plugin", + tool = "tool", + swap = "swap", +} + +export type ApplicationEntity = { + versionId: Id; + versionName: string; + versionDisplayName: string; + version: string; + currencyId: string; + description: string; + applicationType: AppType; + dateModified: string; + icon: string; + authorName: string; + supportURL: string; + contactURL: string; + sourceURL: string; + hash: string; + perso: string; + parentName: string | null; + firmware: string; + firmwareKey: string; + delete: string; + deleteKey: string; + bytes: number; + warning: string | null; + isDevTools: boolean; +}; diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts new file mode 100644 index 000000000..c62f67093 --- /dev/null +++ b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts @@ -0,0 +1,21 @@ +import { MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; + +import { DefaultManagerApiService } from "./DefaultManagerApiService"; +import { ManagerApiService } from "./ManagerApiService"; + +jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); +let dataSource: jest.Mocked; +let service: ManagerApiService; +describe("ManagerApiService", () => { + beforeEach(() => { + dataSource = new DefaultManagerApiDataSource({ + managerApiUrl: MANAGER_API_BASE_URL, + }) as jest.Mocked; + service = new DefaultManagerApiService(dataSource); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts new file mode 100644 index 000000000..442d5125d --- /dev/null +++ b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts @@ -0,0 +1,29 @@ +import { inject, injectable } from "inversify"; + +import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; +import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; +import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes"; +import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; + +import { ManagerApiService } from "./ManagerApiService"; + +@injectable() +export class DefaultManagerApiService implements ManagerApiService { + constructor( + @inject(managerApiTypes.ManagerApiDataSource) + private readonly dataSource: ManagerApiDataSource, + ) { + this.dataSource = dataSource; + } + + getAppsByHash(_apps: ListAppsResponse): Promise { + const hashes = _apps.reduce((acc, app) => { + if (app.appFullHash) { + return acc.concat(app.appFullHash); + } + + return acc; + }, []); + return this.dataSource.getAppsByHash(hashes); + } +} diff --git a/packages/core/src/internal/manager-api/service/ManagerApiService.ts b/packages/core/src/internal/manager-api/service/ManagerApiService.ts new file mode 100644 index 000000000..253dea9de --- /dev/null +++ b/packages/core/src/internal/manager-api/service/ManagerApiService.ts @@ -0,0 +1,6 @@ +import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; +import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; + +export interface ManagerApiService { + getAppsByHash(apps: ListAppsResponse): Promise; +} diff --git a/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts b/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts index b819fb50e..16bc4baa4 100644 --- a/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts +++ b/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts @@ -9,22 +9,38 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { SendApduUseCase } from "@internal/send/use-case/SendApduUseCase"; import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub"; +jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); + let logger: LoggerPublisherService; let sessionService: DeviceSessionService; +let managerApiDataSource: ManagerApiDataSource; +let managerApi: ManagerApiService; const fakeSessionId = "fakeSessionId"; describe("SendApduUseCase", () => { beforeEach(() => { logger = new DefaultLoggerPublisherService([], "send-apdu-use-case"); sessionService = new DefaultDeviceSessionService(() => logger); + managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiUrl: "http://fake.url", + }); + managerApi = new DefaultManagerApiService(managerApiDataSource); }); it("should send an APDU to a connected device", async () => { // given - const deviceSession = deviceSessionStubBuilder({}, () => logger); + const deviceSession = deviceSessionStubBuilder( + {}, + () => logger, + managerApi, + ); sessionService.addDeviceSession(deviceSession); const useCase = new SendApduUseCase(sessionService, () => logger); @@ -63,6 +79,7 @@ describe("SendApduUseCase", () => { const deviceSession = deviceSessionStubBuilder( { connectedDevice }, () => logger, + managerApi, ); sessionService.addDeviceSession(deviceSession); const useCase = new SendApduUseCase(sessionService, () => logger); diff --git a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts b/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts index 939d3bf85..691ed1619 100644 --- a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts +++ b/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts @@ -3,11 +3,19 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { GetConnectedDeviceUseCase } from "@internal/usb/use-case/GetConnectedDeviceUseCase"; import { ConnectedDevice } from "@root/src"; +jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); + let logger: LoggerPublisherService; let sessionService: DeviceSessionService; +let managerApiDataSource: ManagerApiDataSource; +let managerApi: ManagerApiService; const fakeSessionId = "fakeSessionId"; @@ -17,6 +25,10 @@ describe("GetConnectedDevice", () => { [], "get-connected-device-use-case", ); + managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiUrl: "http://fake.url", + }); + managerApi = new DefaultManagerApiService(managerApiDataSource); sessionService = new DefaultDeviceSessionService(() => logger); }); @@ -25,6 +37,7 @@ describe("GetConnectedDevice", () => { const deviceSession = deviceSessionStubBuilder( { id: fakeSessionId }, () => logger, + managerApi, ); sessionService.addDeviceSession(deviceSession); const useCase = new GetConnectedDeviceUseCase(sessionService, () => logger); @@ -43,6 +56,7 @@ describe("GetConnectedDevice", () => { const deviceSession = deviceSessionStubBuilder( { id: fakeSessionId }, () => logger, + managerApi, ); sessionService.addDeviceSession(deviceSession); const useCase = new GetConnectedDeviceUseCase(sessionService, () => logger); diff --git a/packages/signer/context-module/jest.config.ts b/packages/signer/context-module/jest.config.ts index 89d9a684a..175e4572f 100644 --- a/packages/signer/context-module/jest.config.ts +++ b/packages/signer/context-module/jest.config.ts @@ -1,5 +1,11 @@ /* eslint no-restricted-syntax: 0 */ -import type { JestConfigWithTsJest } from "ts-jest"; +import { JestConfigWithTsJest, pathsToModuleNameMapper } from "ts-jest"; + +import { compilerOptions } from "./tsconfig.json"; + +const paths = pathsToModuleNameMapper(compilerOptions.paths, { + prefix: "/", +}); const config: JestConfigWithTsJest = { preset: "@ledgerhq/jest-config-dsdk", @@ -12,8 +18,7 @@ const config: JestConfigWithTsJest = { "!src/api/index.ts", ], moduleNameMapper: { - "^@/(.*)$": "/src/$1", - "^@root/(.*)$": "/$1", + ...paths, }, }; diff --git a/packages/signer/keyring-eth/jest.config.ts b/packages/signer/keyring-eth/jest.config.ts index ac318d966..175e4572f 100644 --- a/packages/signer/keyring-eth/jest.config.ts +++ b/packages/signer/keyring-eth/jest.config.ts @@ -1,5 +1,11 @@ /* eslint no-restricted-syntax: 0 */ -import type { JestConfigWithTsJest } from "ts-jest"; +import { JestConfigWithTsJest, pathsToModuleNameMapper } from "ts-jest"; + +import { compilerOptions } from "./tsconfig.json"; + +const paths = pathsToModuleNameMapper(compilerOptions.paths, { + prefix: "/", +}); const config: JestConfigWithTsJest = { preset: "@ledgerhq/jest-config-dsdk", @@ -12,9 +18,7 @@ const config: JestConfigWithTsJest = { "!src/api/index.ts", ], moduleNameMapper: { - "^@api/(.*)$": "/src/api/$1", - "^@internal/(.*)$": "/src/internal/$1", - "^@root/(.*)$": "/$1", + ...paths, }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 960628d7c..2c9c9c10d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: '@sentry/minimal': specifier: ^6.19.7 version: 6.19.7 + axios: + specifier: ^1.7.2 + version: 1.7.2 inversify: specifier: ^6.0.2 version: 6.0.2 From 73825aaa5869c9026bd1a5a1b142a74a9484662f Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Wed, 24 Jul 2024 17:59:50 +0200 Subject: [PATCH 21/46] =?UTF-8?q?=E2=9C=A8=20(core):=20Add=20ListAppsWithM?= =?UTF-8?q?etadata=20device=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/unlucky-pears-sort.md | 5 + .../__test-utils__/makeInternalApi.ts | 3 +- .../GetDeviceStatusDeviceAction.test.ts | 6 + .../GetDeviceStatusDeviceAction.ts | 2 +- .../GoToDashboardDeviceAction.test.ts | 3 + .../ListAppsWithMetadataDeviceAction.test.ts | 244 +++++++++++++++ .../ListAppsWithMetadataDeviceAction.ts | 289 ++++++++++++++++++ .../os/ListAppsWithMetadata/types.ts | 33 ++ .../OpenAppDeviceAction.test.ts | 1 + .../api/device-session/DeviceSessionState.ts | 6 + .../device-session/model/DeviceSession.ts | 8 +- .../model/DeviceSessionRefresher.ts | 8 +- .../data/DefaultManagerApiDataSource.test.ts | 6 +- .../service/DefaultManagerApiService.test.ts | 2 + 14 files changed, 606 insertions(+), 10 deletions(-) create mode 100644 .changeset/unlucky-pears-sort.md create mode 100644 packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts create mode 100644 packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts create mode 100644 packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts diff --git a/.changeset/unlucky-pears-sort.md b/.changeset/unlucky-pears-sort.md new file mode 100644 index 000000000..cd8f286de --- /dev/null +++ b/.changeset/unlucky-pears-sort.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-sdk-core": minor +--- + +Add ListAppsWithMetadata device action diff --git a/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts b/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts index 70125fe31..880ae7a64 100644 --- a/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts +++ b/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts @@ -1,11 +1,10 @@ import { InternalApi } from "@api/device-action/DeviceAction"; -import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; const sendCommandMock = jest.fn(); const apiGetDeviceSessionStateMock = jest.fn(); const apiGetDeviceSessionStateObservableMock = jest.fn(); const setDeviceSessionStateMock = jest.fn(); -const managerApiServiceMock = jest.fn() as unknown as ManagerApiService; +const managerApiServiceMock = { getAppsByHash: jest.fn() }; export function makeInternalApiMock(): jest.Mocked { return { diff --git a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts index f7c2ba13a..61e7cee57 100644 --- a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts @@ -52,6 +52,7 @@ describe("GetDeviceStatusDeviceAction", () => { sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "mockedCurrentApp", + installedApps: [], }); sendCommandMock.mockResolvedValue({ @@ -98,6 +99,7 @@ describe("GetDeviceStatusDeviceAction", () => { sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.LOCKED, currentApp: "mockedCurrentApp", + installedApps: [], }); apiGetDeviceSessionStateObservableMock.mockImplementation( @@ -111,6 +113,7 @@ describe("GetDeviceStatusDeviceAction", () => { DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "mockedCurrentApp", + installedApps: [], }); o.complete(); } else { @@ -119,6 +122,7 @@ describe("GetDeviceStatusDeviceAction", () => { DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.LOCKED, currentApp: "mockedCurrentApp", + installedApps: [], }); } }, @@ -359,6 +363,7 @@ describe("GetDeviceStatusDeviceAction", () => { DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.LOCKED, currentApp: "mockedCurrentApp", + installedApps: [], }); }, }); @@ -563,6 +568,7 @@ describe("GetDeviceStatusDeviceAction", () => { sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "mockedCurrentApp", + installedApps: [], }); sendCommandMock.mockResolvedValue({ diff --git a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts index d782faae6..f0cecfa64 100644 --- a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts +++ b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts @@ -162,7 +162,7 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< }), }, }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5QHEwBcAiYBuBLAxmAMpoCGaArrFnoQIL5q4D2AdgHQ0FgBKYpEAJ4BiANoAGALqJQAB2axcTNjJAAPRAEYAnOPYAOAGwB2TfoDMh8QBZz4gEyGANCEGIArMe3tN5+-vFte3dzbWt7awBfSJdUTBxuEnIqLnpGFg4AeVYAI2ZSACcIXFYoAGEACzB8AGsxKVV5RWVWVQ0Ea1MDd21zfWD3d2txcXcXNwR3Q3N2P3tdf3tNUfNjaNj0VOIySmoEtJb2bLzC4tLK6rrRTWkkECalDLbETs1u3v7BoZGx10R9N7uEbiTSdTzGcxDTTrEBxLZJXZbBiHAAyzFqJXKVVq9VucgUjxUd3auj04kMwXs5O0+gBIPGHkM3gh2kGEQB-nc+hhcP222Se1oYGRGXYaIx52xVxujQJLWeCFJ7HJlOptOWmgZHVB7GshkMy3CrO0mnmURisM2fIRKT5IrY7AAqrAwAV7axHawADbompbYQQNhgdglbDMGrB52u92en21LYSPH3OVPYlaIEzTzicyaYymVmBTV-bWGAz2ebZrnTV7mjbxIU2wXcd1Ol1u9JsWO+-2ugrMArsWRe8gAM37AFtW9GOx7vd2+YnZc1U6B2poM+wszm8zogSateX7OxtMZDENrFybEs-DyrQ2drahS26LJZHRWBAAGquxRsC44wNWGDUNw2DXl7wFJEZ3YF83w-b8Cl-Vh-xqBAQPwcgMkTRc7geeU0wQKlNDeOxrFpaxdE0ExnGLKZ9B8KZ7Bzdxyz6Exb3rRIHybA5RVg98vx-DIUOEXt+0HYc0DHApJ3ArjILtaD+PgoS-ylNDWDDDCWmwhpcJTIlV0QIiSJscjKOorVLHcdgqRzU0QQvMxOg4+FuKgw4iFIbBhVfBEwADIMcPxZdDPUYzOlLU1vmGLx9G0EwtSsGZaXsU8yJ0cRjHmVzrXcxTPO83zZH80SCj7ApguTULWgIiJTx8YJhlimkEuMLUgWMXV5hMMIyINKZogtVhmAgOBVDkwhGw8ldqsJWqjIQABaGiJiWmztE2rbtq2yxcogxECtFLY+AECYQvmhVwg64x6N8fwLBpIwpnMfb5MOp9oOOfIikxFCl0uur9A2kJiKYuxdEhawbruvxaVCWkz2mN6pvyz7UV9P6pQB-DFuMcJZn6YHjBYsIEuh2j9GsTcKJCSwyNpYGUf5D7m2gqN2xaLt4z5HHZrXZYuq3BHSbCKyQnYUwDTIjKmX6NYLUmlnHzZw5lMExDhOx-SaoVXMz2VEYqLzYYmVPDqWMJ-xyXMaxbGy5npqOh0vJ82D-L5sL2lYmzPCpqYyK8O3fgmKZS2mNKhncYjBj1R20dV0UiAofBCFgeAdcBxa0p1GwIXCL4aSLCZkp8OGzFPYwHAVus3IU9HRQAUXK-tPYW8LCNMam89tgYegBKyAVmHqmqsa3tCGyIgA */ + /** @xstate-layout N4IgpgJg5mDOIC5QHEwBcAiYBuBLAxmAMpoCGaArrFnoQIL5q4D2AdgHQ0FgBKYpEAJ4BiANoAGALqJQAB2axcTNjJAAPRAEYAnOPYAOAGwB2AEzjxmgMzbj+4wBZDAGhCDEAVmPb2107tN9K2D9UwBfMNdUTBxuEnIqLnpGFg4AeVYAI2ZSACcIXFYoAGEACzB8AGsxKVV5RWVWVQ0EB2NNAw9tKyDta3Egq1d3BA9DK3YrU38HcUMPD0sBiKj0JOIySmpY5Mb2DOy8gqKyiurRTWkkEHqlVObENo79Lp6bfsHhxH0Oh21--R9BxGOaGbQrEDRdbxLbrBh7AAyzCqhRK5SqNSucgUdxU1xauj0c1MhmmfSsTxcbk8YPYxhs8ysmk0HiZrIhUJ2GwS21oYHhqXYSJRJ3R50udRxjQeCEJ7GJpP81kpX1amgc7Cchk0QWmL3+xg5ay5MMSXIFbHYAFVYGBchbWFbWAAbZGVdbCCBsMDsQrYZiVH02u0Op2uqrrCRYm5S+74rSLCZecSzbTjGzBYyqhyaQwGMltPpdcQecKRSHGvmm3ncB3W232lJsMNuj123LMXLsWTO8gAM07AFt6yGm46Xa2uVHJQ046AWizxEnjCnxGngt0rFnqQhpqZ2LZ5trjCvFiYjTEq5szXy63RZLI6KwIAA1O2KNinDFe1g+v0Bn1OSvHk4THdh70fZ831yD9WC-SoEH-fByFSKNp2uW5pXjXdLA6KwU30HMSz6dV9FVMZ9F8MZTDGcRTH6NML2ha8a12QUIKfV931SeDhHbTtu17NAB1yYcgLiFjQL2DioO4z8xUQ1h-WQxo0NqDDYzxedEHMZlJgIoiumZYFVSseZ2HMJkmToswzAWJiTUk80wKIUhsH5B8YTAT1vXQ7FZy09QdLaPNNBoilNBXOj-g8VVDCXAxQlCVktyXGwHOA2FnL2Vz3Igry+NyDtcj8mMAqabDTBC3xwpzKL-G0WKd0WYxNX8erCKXQiInLVhmAgOBVHEwhqykucytxCrtIQABaKkRhmjwD3+FbVpW7wMokkDssFdY+AEEZ-MmmUHFMci7F8KZCKqrdCJ6TaRqc28wIOHJ8lReCZ2OyqXgPVkWRsNcHC6BxzsovxrocW6of0B7uSy57ETdD6xS+rDpscfcekCRqdVCNLyMI9gPD+LxbIiw1y2G+Gb1rMDg0bRoWwjLk0fGhdNHEVrkwcVNIu6UxtxGKxWTpXNTApfwPB0Lw4dGnbLRkriYJ41GNPKmVIvMixcO0HNgnMebPFLSYkrC0tFlmSnVkvLaEbpnK3I82QvLZwKWmmEXibsWYpglxrWXIww83GQIWXaKZ-gcOWnodwUiAofBCFgeB1e+6bBfVeUniBU6T20OKEohgZkymHQY+2xHBQAUSKzs3amoLd3aDUU3aXPBe8Uyfkmdqxka2wix6sIgA */ id: "GetDeviceStatusDeviceAction", initial: "DeviceReady", context: (_) => { diff --git a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts b/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts index efaa33213..0c111cbf7 100644 --- a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts @@ -86,6 +86,7 @@ describe("GoToDashboardDeviceAction", () => { sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "BOLOS", + installedApps: [], }); const expectedStates: Array = [ @@ -135,6 +136,7 @@ describe("GoToDashboardDeviceAction", () => { sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "Bitcoin", + installedApps: [], }); sendCommandMock.mockResolvedValueOnce(undefined).mockResolvedValueOnce({ @@ -312,6 +314,7 @@ describe("GoToDashboardDeviceAction", () => { sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "BOLOS", + installedApps: [], }); const expectedStates: Array = [ diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts new file mode 100644 index 000000000..0a949ab8c --- /dev/null +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts @@ -0,0 +1,244 @@ +import { Left, Right } from "purify-ts"; +import { assign, createMachine } from "xstate"; + +import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; +import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; +import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; +import { UnknownDAError } from "@api/device-action/os/Errors"; +import { ListAppsDeviceAction } from "@api/device-action/os/ListApps/ListAppsDeviceAction"; +import { AppType } from "@internal/manager-api/model/ManagerApiResponses"; + +import { ListAppsWithMetadataDeviceAction } from "./ListAppsWithMetadataDeviceAction"; +import { ListAppsWithMetadataDAState } from "./types"; + +jest.mock("@api/device-action/os/ListApps/ListAppsDeviceAction"); + +const BTC_APP = { + appEntryLength: 77, + appSizeInBlocks: 3227, + appCodeHash: + "924b5ba590971b3e98537cf8241f0aa51b1e6f26c37915dd38b83255168255d5", + appFullHash: + "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", + appName: "Bitcoin", +}; + +const BTC_APP_METADATA = { + versionId: 36248, + versionName: "Bitcoin", + versionDisplayName: "Bitcoin", + version: "2.2.2", + currencyId: "bitcoin", + description: "", + applicationType: AppType.currency, + dateModified: "2024-04-08T11:31:34.847313Z", + icon: "bitcoin", + authorName: " Ledger", + supportURL: + "https://support.ledger.com/hc/en-us/articles/115005195945-Bitcoin-BTC-", + contactURL: "mailto:https://support.ledger.com/hc/en-us/requests/new", + sourceURL: "https://github.com/LedgerHQ/app-bitcoin-new", + compatibleWallets: + '[ { "name": "Electrum", "url": "https://electrum.org/#home" } ]', + hash: "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", + perso: "perso_11", + firmware: "stax/1.4.0-rc2/bitcoin/app_2.2.2", + firmwareKey: "stax/1.4.0-rc2/bitcoin/app_2.2.2_key", + delete: "stax/1.4.0-rc2/bitcoin/app_2.2.2_del", + deleteKey: "stax/1.4.0-rc2/bitcoin/app_2.2.2_del_key", + bytes: 103264, + warning: null, + isDevTools: false, + category: 1, + parent: null, + parentName: null, +}; + +// const CUSTOM_LOCK_SCREEN_APP = { +// appEntryLength: 70, +// appSizeInBlocks: 1093, +// appCodeHash: +// "0000000000000000000000000000000000000000000000000000000000000000", +// appFullHash: +// "5602b3d3fdde77fc02eb451a8beec4155bcf8b83ced794d7b3c63afaed5ff8c6", +// appName: "", +// }; + +// const CUSTOM_LOCK_SCREEN_APP_METADATA = null; + +// const ETH_APP = { +// appEntryLength: 78, +// appSizeInBlocks: 4120, +// appCodeHash: +// "4fdb751c0444f3a982c2ae9dcfde6ebe6dab03613d496f5e53cf91bce8ca46b5", +// appFullHash: +// "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", +// appName: "Ethereum", +// }; + +// const ETH_APP_METADATA = { +// versionId: 36185, +// versionName: "Ethereum", +// versionDisplayName: "Ethereum", +// version: "1.10.4", +// currencyId: "ethereum", +// description: "", +// applicationType: AppType.currency, +// dateModified: "2024-04-09T12:28:55.783551Z", +// icon: "ethereum", +// authorName: " Ledger", +// supportURL: +// "https://support.ledger.com/hc/en-us/articles/360009576554-Ethereum-ETH-", +// contactURL: "mailto:https://support.ledger.com/hc/en-us/requests/new", +// sourceURL: "https://github.com/LedgerHQ/app-ethereum", +// compatibleWallets: +// '[ { "name": "Metamask", "url": "https://metamask.io/" }, { "name": "Phantom", "url": "https://phantom.app/" }, { "name": "Rabby", "url": "https://rabby.io/" }, { "name": "Rainbow", "url": "https://rainbow.me/" }, { "name": "MyCrypto", "url": "https://www.ledger.com/mycrypto/" }, { "name": "MyEtherWallet", "url": "https://www.ledger.com/myetherwallet/" } ]', +// hash: "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", +// perso: "perso_11", +// firmware: "stax/1.4.0-rc3/ethereum/app_1.10.4", +// firmwareKey: "stax/1.4.0-rc3/ethereum/app_1.10.4_key", +// delete: "stax/1.4.0-rc3/ethereum/app_1.10.4_del", +// deleteKey: "stax/1.4.0-rc3/ethereum/app_1.10.4_del_key", +// bytes: 131852, +// warning: "", +// isDevTools: false, +// category: 1, +// parent: null, +// parentName: null, +// }; + +type App = typeof BTC_APP; + +const setupListAppsMock = (apps: App[], error = false) => { + (ListAppsDeviceAction as jest.Mock).mockImplementation(() => ({ + makeStateMachine: jest.fn().mockImplementation(() => + createMachine({ + id: "MockListAppsDeviceAction", + initial: "ready", + states: { + ready: { + after: { + 0: "done", + }, + entry: assign({ + intermediateValue: () => ({ + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }), + }), + }, + done: { + type: "final", + }, + }, + output: () => { + return error + ? Left(new UnknownDAError("ListApps failed")) + : Right(apps); + }, + }), + ), + })); +}; + +describe("ListAppsWithMetadataDeviceAction", () => { + const { + managerApiService: managerApiServiceMock, + // getDeviceSessionState: apiGetDeviceSessionStateMock, + // setDeviceSessionState: apiSetDeviceSessionStateMock, + } = makeInternalApiMock(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe("success case", () => { + it("should run the device actions with no apps installed", (done) => { + setupListAppsMock([]); + const listAppsWithMetadataDeviceAction = + new ListAppsWithMetadataDeviceAction({ + input: {}, + }); + + jest.spyOn(managerApiServiceMock, "getAppsByHash").mockResolvedValue([]); + + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Ready + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }, + status: DeviceActionStatus.Pending, // ListAppsDeviceAction + }, + { + status: DeviceActionStatus.Completed, + output: [], + }, + ]; + + testDeviceActionStates( + listAppsWithMetadataDeviceAction, + expectedStates, + makeInternalApiMock(), + done, + ); + }); + + it("should run the device actions with 1 app installed", (done) => { + setupListAppsMock([BTC_APP]); + const listAppsWithMetadataDeviceAction = + new ListAppsWithMetadataDeviceAction({ + input: {}, + }); + + jest + .spyOn(managerApiServiceMock, "getAppsByHash") + .mockResolvedValue([BTC_APP_METADATA]); + + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Ready + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }, + status: DeviceActionStatus.Pending, // ListAppsDeviceAction + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // ListAppsChecks + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Success + }, + { + status: DeviceActionStatus.Completed, + output: [BTC_APP_METADATA], + }, + ]; + + testDeviceActionStates( + listAppsWithMetadataDeviceAction, + expectedStates, + makeInternalApiMock(), + done, + ); + }); + }); + + // TODO: finish testing +}); diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts new file mode 100644 index 000000000..f078a0620 --- /dev/null +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts @@ -0,0 +1,289 @@ +import { Left, Right } from "purify-ts"; +import { + AnyEventObject, + assign, + fromCallback, + fromPromise, + setup, +} from "xstate"; + +import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; +import { InternalApi } from "@api/device-action/DeviceAction"; +import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; +import { UnknownDAError } from "@api/device-action/os/Errors"; +import { ListAppsDeviceAction } from "@api/device-action/os/ListApps/ListAppsDeviceAction"; +import { ListAppsDAOutput } from "@api/device-action/os/ListApps/types"; +import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes"; +import { XStateDeviceAction } from "@api/device-action/xstate-utils/XStateDeviceAction"; +import { DeviceSessionState } from "@api/device-session/DeviceSessionState"; +import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; + +import { + ListAppsWithMetadataDAError, + ListAppsWithMetadataDAInput, + ListAppsWithMetadataDAIntermediateValue, + ListAppsWithMetadataDAOutput, +} from "./types"; + +type ListAppsWithMetadataMachineInternalState = { + error: ListAppsWithMetadataDAError | null; + apps: ListAppsResponse; + appsWithMetadata: ListAppsWithMetadataDAOutput; +}; + +export type MachineDependencies = { + getAppsByHash: ({ + input, + }: { + input: ListAppsDAOutput; + }) => Promise; + getDeviceSessionState: () => DeviceSessionState; + saveSessionState: (state: DeviceSessionState) => DeviceSessionState; +}; + +export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< + ListAppsWithMetadataDAOutput, + ListAppsWithMetadataDAInput, + ListAppsWithMetadataDAError, + ListAppsWithMetadataDAIntermediateValue, + ListAppsWithMetadataMachineInternalState +> { + makeStateMachine(internalAPI: InternalApi) { + type types = StateMachineTypes< + ListAppsWithMetadataDAOutput, + ListAppsWithMetadataDAInput, + ListAppsWithMetadataDAError, + ListAppsWithMetadataDAIntermediateValue, + ListAppsWithMetadataMachineInternalState + >; + + const { getAppsByHash, saveSessionState, getDeviceSessionState } = + this.extractDependencies(internalAPI); + + const unlockTimeout = this.input.unlockTimeout ?? 0; + + const listAppsMachine = new ListAppsDeviceAction({ + input: { + unlockTimeout, + }, + }).makeStateMachine(internalAPI); + + return setup({ + types: { + input: { + unlockTimeout, + } as types["input"], + context: {} as types["context"], + output: {} as types["output"], + }, + actors: { + listApps: listAppsMachine, + getAppsByHash: fromPromise( + getAppsByHash, + ), + saveSessionState: fromCallback( + ({ + input, + sendBack, + }: { + sendBack: (event: AnyEventObject) => void; + input: { + appsWithMetadata: ApplicationEntity[]; + }; + }) => { + const { appsWithMetadata } = input; + if (!appsWithMetadata) { + return sendBack({ type: "error" }); + } + + const sessionState = getDeviceSessionState(); + const updatedState = { + ...sessionState, + installedApps: appsWithMetadata, + }; + saveSessionState(updatedState); + sendBack({ type: "done" }); + }, + ), + }, + guards: { + hasError: ({ context }: { context: types["context"] }) => { + return context._internalState.error !== null; + }, + hasNoAppsInstalled: ({ context }: { context: types["context"] }) => + context._internalState.apps.length === 0, + }, + actions: { + assignErrorFromEvent: assign({ + _internalState: (_) => ({ + ..._.context._internalState, + error: _.event["error"], // FIXME: add a typeguard + }), + }), + assignErrorSaveAppState: assign({ + _internalState: (_) => ({ + ..._.context._internalState, + error: new UnknownDAError("SaveSession Error"), + }), + }), + }, + }).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QBkCWsAuBBADj2A6qhgBYCyYGAhhFdQCJgBuqAxmFqxqgPYB2AOkYt2AJTA0AngGIA2gAYAuolA4esYrz4qQAD0QBGAJzyBADgBsAdgNX5Fow6NWjAGhCTDBswKsAmCwBWPwAWPwBmM0DjAwBfWPc0TFx8IlIKaloGZjYOLi0BJOw8WGkIfjABVD4mHgBrSoAbdGL8BWUkEDUNbn4dfQRgnyM-b0D5Kwt5EKsbd08EUKsBKYNAswn7eTMrMz94xJaUwmJySho6KmFczl7BIuPpMAAnZ55ngRxGugAzd4BbATNZIldo6bqaPqdAZDAQjMYTKYzOYeRBmAwCEJGbEWCzo+QGeQTA4gB4lNJnTKXa7sW4FMn4ADCJDArDqciU4PUkO00MQRgMGJCZhC4XCuMmAXk4XmiHWgUx9j8IqigUCeLiCVJR3JpwyF2yIjyd0KOqZLLZcgMHVU3Lu-X5gsxIrFEosUplqIQFnCQvsvuiyvsFhCJIZJ3S5yyVxytPy-FNIPNrPZsj8Nq6dq0DoQAqFLvF1ndU09CwCGLs4W2gRC42xzisYbNEcpBpjRrpCYAYpRWK3o2UKlUavVKjBWrAAEKSAASVFgJDBnQh9r5CDW8gVPr8diJBPGQVlCEiFelBnCJjMF4F+y14Yp+ujNONBR7GD7j8uT1e70+3wwfzPIC47HNOc4Lkuto9Nma4blu4Q7nu+72IER5+BEvgBIE4RWMKjhWIERjhE2SYtp+ho3PGggAMpUEwYDUXAGj8IOfBgJBmbQVCoADMYNYCIEsyWMEwZFkeITuoq25BKMZiWLehykQ+UbUrGL4JrR9GMbAzF8N+bzPBxK4wTxhhGPxgm7DJomTEe4oKuZvrbAYDh+NEazxFqfA8BAcA6PeeoqRRcarlBPI5gAtBYR5RQISHhBJB4mOeRgkROylUsF6mCM+4hSFyXG8qZCBhEegrLP4QSirYiHoqld7NhlbbPp29zNgV4VrsqDnYYSV4GH48joX4R5GCEAjoUEwRBOKGwhKGDVKYFmXtpRJrhsyKYdaFeiIFY4rmLhziipYU1uF6ARGAJuyEv4xiTO6aXHE1T5qa1Ahvh+QVUNtJm7euLnhJi2IiiMRLhNNaEHbWvoRGKCFqj6T26pGK0tVRAiaQxTF-cZ3H-bYBjjWeRNVg4FhE3ZGwCLYzgI-I5n7bYyOpMtzVvRj1EAK6sOwOm-fjAxwwqwSRP4bmilYR7RBYNOzc4uJquemqKelbOvR2GMAKI-s8AtFf9sl+AJvqEuKY12DsaFYnF2HVrWREBJEnmxEAA */ + id: "ListAppsWithMetadataDeviceAction", + initial: "DeviceReady", + context: (_) => { + return { + input: _.input, + _internalState: { + error: null, + apps: [], + appsWithMetadata: [], + }, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }; + }, + states: { + DeviceReady: { + always: { + target: "ListApps", + }, + }, + ListApps: { + invoke: { + id: "listApps", + src: "listApps", + input: (_) => ({ + unlockTimeout: _.context.input.unlockTimeout, + }), + onSnapshot: { + actions: assign({ + intermediateValue: (_) => + _.event.snapshot.context.intermediateValue, + }), + }, + onDone: { + target: "ListAppsCheck", + actions: assign({ + intermediateValue: (_) => ({ + requiredUserInteraction: UserInteractionRequired.None, + }), + _internalState: (_) => { + return _.event.output.caseOf({ + Right: (apps) => ({ + ..._.context._internalState, + apps, + }), + Left: (error) => ({ + ..._.context._internalState, + error, + }), + }); + }, + }), + }, + onError: { + target: "Error", + actions: [ + "assignErrorFromEvent", + assign({ + intermediateValue: (_) => ({ + requiredUserInteraction: UserInteractionRequired.None, + }), + }), + ], + }, + }, + }, + ListAppsCheck: { + always: [ + { + target: "Error", + guard: "hasError", + }, + { + target: "Success", + guard: "hasNoAppsInstalled", + actions: assign({ + _internalState: (_) => { + return { + ..._.context._internalState, + appsWithMetadata: [], + }; + }, + }), + }, + { + target: "FetchMetadata", + }, + ], + }, + FetchMetadata: { + invoke: { + id: "getAppsByHash", + src: "getAppsByHash", + input: (_) => _.context._internalState.apps, + onDone: { + target: "SaveSession", + actions: assign({ + _internalState: (_) => { + return { + ..._.context._internalState, + appsWithMetadata: _.event.output, + }; + }, + }), + }, + onError: { + target: "Error", + actions: "assignErrorFromEvent", + }, + }, + }, + SaveSession: { + invoke: { + src: "saveSessionState", + input: (_) => ({ + appsWithMetadata: _.context._internalState.appsWithMetadata, + }), + }, + on: { + done: { + target: "Success", + }, + error: { + target: "Error", + actions: "assignErrorFromEvent", + }, + }, + }, + Success: { + type: "final", + }, + Error: { + type: "final", + }, + }, + output: (_) => { + if (_.context._internalState.error) { + return Left(_.context._internalState.error); + } + + return Right(_.context._internalState.appsWithMetadata); + }, + }); + } + + extractDependencies(internalApi: InternalApi): MachineDependencies { + return { + getAppsByHash: async ({ input }) => { + const res = await internalApi.managerApiService.getAppsByHash(input); + return res; + }, + getDeviceSessionState: () => internalApi.getDeviceSessionState(), + saveSessionState: (state: DeviceSessionState) => + internalApi.setDeviceSessionState(state), + }; + } +} diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts new file mode 100644 index 000000000..91d93065d --- /dev/null +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts @@ -0,0 +1,33 @@ +import { DeviceActionState } from "@api/device-action/model/DeviceActionState"; +import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; +import { UnknownDAError } from "@api/device-action/os/Errors"; +import { + ListAppsDAError, + ListAppsDAInput, + ListAppsDAIntermediateValue, +} from "@api/device-action/os/ListApps/types"; +import { SdkError } from "@api/Error"; +import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; + +export type ListAppsWithMetadataDAOutput = ApplicationEntity[]; +export type ListAppsWithMetadataDAInput = ListAppsDAInput; + +export type ListAppsWithMetadataDAError = + | ListAppsDAError + | UnknownDAError + | SdkError; /// TODO: remove, we should have an exhaustive list of errors + +export type ListAppsWithMetadataDARequiredInteraction = + UserInteractionRequired.None; + +export type ListAppsWithMetadataDAIntermediateValue = + | ListAppsDAIntermediateValue + | { + requiredUserInteraction: ListAppsWithMetadataDARequiredInteraction; + }; + +export type ListAppsWithMetadataDAState = DeviceActionState< + ListAppsWithMetadataDAOutput, + ListAppsWithMetadataDAError, + ListAppsWithMetadataDAIntermediateValue +>; diff --git a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts b/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts index 6ebfc8852..36dc2f852 100644 --- a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts @@ -46,6 +46,7 @@ describe("OpenAppDeviceAction", () => { sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: "Bitcoin", + installedApps: [], }); sendCommandMock.mockResolvedValueOnce({ diff --git a/packages/core/src/api/device-session/DeviceSessionState.ts b/packages/core/src/api/device-session/DeviceSessionState.ts index ed047a70f..7bf3ec628 100644 --- a/packages/core/src/api/device-session/DeviceSessionState.ts +++ b/packages/core/src/api/device-session/DeviceSessionState.ts @@ -1,5 +1,6 @@ import { BatteryStatusFlags } from "@api/command/os/GetBatteryStatusCommand"; import { DeviceStatus } from "@api/device/DeviceStatus"; +import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; /** * The battery status of a device. @@ -72,6 +73,11 @@ type DeviceSessionReadyState = { * The current application running on the device. */ readonly currentApp: string; + + /** + * The current applications installed on the device. + */ + readonly installedApps: ApplicationEntity[]; }; /** diff --git a/packages/core/src/internal/device-session/model/DeviceSession.ts b/packages/core/src/internal/device-session/model/DeviceSession.ts index fc08f6594..e78515b6d 100644 --- a/packages/core/src/internal/device-session/model/DeviceSession.ts +++ b/packages/core/src/internal/device-session/model/DeviceSession.ts @@ -57,8 +57,12 @@ export class DeviceSession { isPolling: true, triggersDisconnection: false, }), - updateStateFn: (state: DeviceSessionState) => - this.setDeviceSessionState(state), + updateStateFn: (fn) => { + const state = this._deviceState.getValue(); + this.setDeviceSessionState(fn(state)); + }, + // updateStateFn: (state: DeviceSessionState) => + // this.setDeviceSessionState(state), }, loggerModuleFactory("device-session-refresher"), ); diff --git a/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts b/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts index b31e98deb..33394fda5 100644 --- a/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts +++ b/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts @@ -39,7 +39,7 @@ export type DeviceSessionRefresherArgs = { * polling response. * @param state - The new state to update to. */ - updateStateFn(state: DeviceSessionState): void; + updateStateFn(fn: (state: DeviceSessionState) => DeviceSessionState): void; }; /** @@ -103,11 +103,13 @@ export class DeviceSessionRefresher { return; } // `batteryStatus` and `firmwareVersion` are not available in the polling response. - updateStateFn({ + updateStateFn((state) => ({ + ...state, sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: this._deviceStatus, currentApp: parsedResponse.name, - }); + installedApps: "installedApps" in state ? state.installedApps : [], + })); }); } diff --git a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts index e17f3ba83..32b315319 100644 --- a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts +++ b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts @@ -10,12 +10,14 @@ describe("DefaultManagerApiDataSource", () => { // appFullHash from the ListApps/ListAppsContinue command's response const hashes = [ - "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", + "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", ]; const apps = await api.getAppsByHash(hashes); - // console.log(apps); + console.log(apps); expect(apps).toHaveLength(1); }); + + // TODO: finish testing }); diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts index c62f67093..89f8ae02d 100644 --- a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts +++ b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts @@ -18,4 +18,6 @@ describe("ManagerApiService", () => { it("should be defined", () => { expect(service).toBeDefined(); }); + + // TODO: finish testing }); From 3b6ae716acb0766e2455db2299c3b1099434342d Mon Sep 17 00:00:00 2001 From: Olivier Freyssinet Date: Mon, 29 Jul 2024 10:08:36 +0200 Subject: [PATCH 22/46] =?UTF-8?q?=F0=9F=9A=9A=20(core):=20Export=20ListApp?= =?UTF-8?q?sWithMetadataDeviceAction=20&=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/api/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 6ee653a01..46ffe7067 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -69,6 +69,14 @@ export { type ListAppsDAOutput, type ListAppsDAState, } from "@api/device-action/os/ListApps/types"; +export { ListAppsWithMetadataDeviceAction } from "@api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction"; +export { + type ListAppsWithMetadataDAError, + type ListAppsWithMetadataDAInput, + type ListAppsWithMetadataDAIntermediateValue, + type ListAppsWithMetadataDAOutput, + type ListAppsWithMetadataDAState, +} from "@api/device-action/os/ListAppsWithMetadata/types"; export { OpenAppDeviceAction } from "@api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction"; export { type OpenAppDAError, From f290f0ee2ffd899ba63c965d8d511904174cc008 Mon Sep 17 00:00:00 2001 From: ofreyssinet-ledger Date: Mon, 29 Jul 2024 10:09:11 +0200 Subject: [PATCH 23/46] =?UTF-8?q?=E2=9C=A8=20(sample):=20Add=20ListAppsWit?= =?UTF-8?q?hMetadataDeviceAction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/chatty-waves-live.md | 5 + .../components/DeviceActionsView/index.tsx | 197 ++++++++++-------- 2 files changed, 118 insertions(+), 84 deletions(-) create mode 100644 .changeset/chatty-waves-live.md diff --git a/.changeset/chatty-waves-live.md b/.changeset/chatty-waves-live.md new file mode 100644 index 000000000..13974c707 --- /dev/null +++ b/.changeset/chatty-waves-live.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-sdk-sample": patch +--- + +Add ListAppsWithMetadataDeviceAction in sample app diff --git a/apps/sample/src/components/DeviceActionsView/index.tsx b/apps/sample/src/components/DeviceActionsView/index.tsx index 87f2fe508..cdcbfbe06 100644 --- a/apps/sample/src/components/DeviceActionsView/index.tsx +++ b/apps/sample/src/components/DeviceActionsView/index.tsx @@ -24,6 +24,11 @@ import { ListAppsDAOutput, ListAppsDAError, ListAppsDAIntermediateValue, + ListAppsWithMetadataDeviceAction, + ListAppsWithMetadataDAError, + ListAppsWithMetadataDAInput, + ListAppsWithMetadataDAIntermediateValue, + ListAppsWithMetadataDAOutput, } from "@ledgerhq/device-sdk-core"; const UNLOCK_TIMEOUT = 60 * 1000; // 1 minute @@ -35,90 +40,114 @@ export const DeviceActionsView: React.FC<{ sessionId: string }> = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any const deviceActions: DeviceActionProps[] = useMemo( - () => [ - { - title: "Open app", - description: - "Perform all the actions necessary to open an app on the device", - executeDeviceAction: ({ appName }, inspect) => { - const deviceAction = new OpenAppDeviceAction({ - input: { appName }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { appName: "" }, - } satisfies DeviceActionProps< - OpenAppDAOutput, - OpenAppDAInput, - OpenAppDAError, - OpenAppDAIntermediateValue - >, - { - title: "Get device status", - description: - "Perform various checks on the device to determine its status", - executeDeviceAction: ({ unlockTimeout }, inspect) => { - const deviceAction = new GetDeviceStatusDeviceAction({ - input: { unlockTimeout }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, - } satisfies DeviceActionProps< - GetDeviceStatusDAOutput, - GetDeviceStatusDAInput, - GetDeviceStatusDAError, - GetDeviceStatusDAIntermediateValue - >, - { - title: "Go to dashboard", - description: "Navigate to the dashboard", - executeDeviceAction: (_, inspect) => { - const deviceAction = new GoToDashboardDeviceAction({ - input: { unlockTimeout: UNLOCK_TIMEOUT }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, - } satisfies DeviceActionProps< - GoToDashboardDAOutput, - GoToDashboardDAInput, - GoToDashboardDAError, - GoToDashboardDAIntermediateValue - >, - { - title: "List apps", - description: "List all applications installed on the device", - executeDeviceAction: (_, inspect) => { - const deviceAction = new ListAppsDeviceAction({ - input: { unlockTimeout: UNLOCK_TIMEOUT }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, - } satisfies DeviceActionProps< - ListAppsDAOutput, - ListAppsDAInput, - ListAppsDAError, - ListAppsDAIntermediateValue - >, - ], + () => + !selectedSessionId + ? [] + : [ + { + title: "Open app", + description: + "Perform all the actions necessary to open an app on the device", + executeDeviceAction: ({ appName }, inspect) => { + const deviceAction = new OpenAppDeviceAction({ + input: { appName }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { appName: "" }, + } satisfies DeviceActionProps< + OpenAppDAOutput, + OpenAppDAInput, + OpenAppDAError, + OpenAppDAIntermediateValue + >, + { + title: "Get device status", + description: + "Perform various checks on the device to determine its status", + executeDeviceAction: ({ unlockTimeout }, inspect) => { + const deviceAction = new GetDeviceStatusDeviceAction({ + input: { unlockTimeout }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, + } satisfies DeviceActionProps< + GetDeviceStatusDAOutput, + GetDeviceStatusDAInput, + GetDeviceStatusDAError, + GetDeviceStatusDAIntermediateValue + >, + { + title: "Go to dashboard", + description: "Navigate to the dashboard", + executeDeviceAction: (_, inspect) => { + const deviceAction = new GoToDashboardDeviceAction({ + input: { unlockTimeout: UNLOCK_TIMEOUT }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, + } satisfies DeviceActionProps< + GoToDashboardDAOutput, + GoToDashboardDAInput, + GoToDashboardDAError, + GoToDashboardDAIntermediateValue + >, + { + title: "List apps", + description: "List all applications installed on the device", + executeDeviceAction: (_, inspect) => { + const deviceAction = new ListAppsDeviceAction({ + input: { unlockTimeout: UNLOCK_TIMEOUT }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, + } satisfies DeviceActionProps< + ListAppsDAOutput, + ListAppsDAInput, + ListAppsDAError, + ListAppsDAIntermediateValue + >, + { + title: "List apps with metadata", + description: + "List all applications installed on the device with additional metadata", + executeDeviceAction: (_, inspect) => { + const deviceAction = new ListAppsWithMetadataDeviceAction({ + input: { unlockTimeout: UNLOCK_TIMEOUT }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, + } satisfies DeviceActionProps< + ListAppsWithMetadataDAOutput, + ListAppsWithMetadataDAInput, + ListAppsWithMetadataDAError, + ListAppsWithMetadataDAIntermediateValue + >, + ], [], ); From ee5e9d21ebf5471a50aa2db826a630009a726aa3 Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Mon, 29 Jul 2024 11:11:04 +0200 Subject: [PATCH 24/46] =?UTF-8?q?=E2=9C=85=20(core):=20Add=20tests=20+=20u?= =?UTF-8?q?sing=20Either=20in=20ManagerApi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DeviceActionsView/index.tsx | 213 ++++---- .../core-module/with-prompt/di-module.ejs.t | 2 +- packages/core/src/api/DeviceSdk.test.ts | 31 +- packages/core/src/api/DeviceSdk.ts | 2 +- .../api/device-action/__test-utils__/data.ts | 108 +++++ .../__test-utils__/setupTestMachine.ts | 109 +++++ .../GetDeviceStatusDeviceAction.test.ts | 4 +- .../GoToDashboardDeviceAction.test.ts | 40 +- .../os/ListApps/ListAppsDeviceAction.test.ts | 95 +--- .../ListAppsWithMetadataDeviceAction.test.ts | 457 ++++++++++++------ .../ListAppsWithMetadataDeviceAction.ts | 80 +-- .../os/ListAppsWithMetadata/types.ts | 2 +- .../SendCommandInAppDeviceAction.test.ts | 20 +- packages/core/src/di.ts | 9 +- .../internal/config/di/configModule.test.ts | 2 +- .../src/internal/config/di/configModule.ts | 4 +- .../device-model/di/deviceModelModule.test.ts | 2 +- .../device-model/di/deviceModelModule.ts | 4 +- .../device-session/model/DeviceSession.ts | 7 +- .../model/DeviceSessionRefresher.ts | 7 +- .../discovery/di/discoveryModule.test.ts | 6 +- .../internal/discovery/di/discoveryModule.ts | 4 +- .../data/DefaultManagerApiDataSource.test.ts | 93 +++- .../data/DefaultManagerApiDataSource.ts | 17 +- .../manager-api/data/ManagerApiDataSource.ts | 7 +- .../manager-api/di/managerApiModule.test.ts | 36 +- .../manager-api/di/managerApiModule.ts | 7 +- .../src/internal/manager-api/model/Errors.ts | 10 + .../service/DefaultManagerApiService.test.ts | 60 ++- .../service/DefaultManagerApiService.ts | 27 +- .../manager-api/service/ManagerApiService.ts | 7 +- .../src/internal/send/di/sendModule.test.ts | 2 +- .../core/src/internal/send/di/sendModule.ts | 4 +- .../src/internal/usb/di/usbModule.test.ts | 4 +- .../core/src/internal/usb/di/usbModule.ts | 4 +- 35 files changed, 979 insertions(+), 507 deletions(-) create mode 100644 packages/core/src/api/device-action/__test-utils__/data.ts create mode 100644 packages/core/src/api/device-action/__test-utils__/setupTestMachine.ts create mode 100644 packages/core/src/internal/manager-api/model/Errors.ts diff --git a/apps/sample/src/components/DeviceActionsView/index.tsx b/apps/sample/src/components/DeviceActionsView/index.tsx index cdcbfbe06..798ce20d6 100644 --- a/apps/sample/src/components/DeviceActionsView/index.tsx +++ b/apps/sample/src/components/DeviceActionsView/index.tsx @@ -40,114 +40,111 @@ export const DeviceActionsView: React.FC<{ sessionId: string }> = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any const deviceActions: DeviceActionProps[] = useMemo( - () => - !selectedSessionId - ? [] - : [ - { - title: "Open app", - description: - "Perform all the actions necessary to open an app on the device", - executeDeviceAction: ({ appName }, inspect) => { - const deviceAction = new OpenAppDeviceAction({ - input: { appName }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { appName: "" }, - } satisfies DeviceActionProps< - OpenAppDAOutput, - OpenAppDAInput, - OpenAppDAError, - OpenAppDAIntermediateValue - >, - { - title: "Get device status", - description: - "Perform various checks on the device to determine its status", - executeDeviceAction: ({ unlockTimeout }, inspect) => { - const deviceAction = new GetDeviceStatusDeviceAction({ - input: { unlockTimeout }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, - } satisfies DeviceActionProps< - GetDeviceStatusDAOutput, - GetDeviceStatusDAInput, - GetDeviceStatusDAError, - GetDeviceStatusDAIntermediateValue - >, - { - title: "Go to dashboard", - description: "Navigate to the dashboard", - executeDeviceAction: (_, inspect) => { - const deviceAction = new GoToDashboardDeviceAction({ - input: { unlockTimeout: UNLOCK_TIMEOUT }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, - } satisfies DeviceActionProps< - GoToDashboardDAOutput, - GoToDashboardDAInput, - GoToDashboardDAError, - GoToDashboardDAIntermediateValue - >, - { - title: "List apps", - description: "List all applications installed on the device", - executeDeviceAction: (_, inspect) => { - const deviceAction = new ListAppsDeviceAction({ - input: { unlockTimeout: UNLOCK_TIMEOUT }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, - } satisfies DeviceActionProps< - ListAppsDAOutput, - ListAppsDAInput, - ListAppsDAError, - ListAppsDAIntermediateValue - >, - { - title: "List apps with metadata", - description: - "List all applications installed on the device with additional metadata", - executeDeviceAction: (_, inspect) => { - const deviceAction = new ListAppsWithMetadataDeviceAction({ - input: { unlockTimeout: UNLOCK_TIMEOUT }, - inspect, - }); - return sdk.executeDeviceAction({ - sessionId: selectedSessionId, - deviceAction, - }); - }, - initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, - } satisfies DeviceActionProps< - ListAppsWithMetadataDAOutput, - ListAppsWithMetadataDAInput, - ListAppsWithMetadataDAError, - ListAppsWithMetadataDAIntermediateValue - >, - ], + () => [ + { + title: "Open app", + description: + "Perform all the actions necessary to open an app on the device", + executeDeviceAction: ({ appName }, inspect) => { + const deviceAction = new OpenAppDeviceAction({ + input: { appName }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { appName: "" }, + } satisfies DeviceActionProps< + OpenAppDAOutput, + OpenAppDAInput, + OpenAppDAError, + OpenAppDAIntermediateValue + >, + { + title: "Get device status", + description: + "Perform various checks on the device to determine its status", + executeDeviceAction: ({ unlockTimeout }, inspect) => { + const deviceAction = new GetDeviceStatusDeviceAction({ + input: { unlockTimeout }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, + } satisfies DeviceActionProps< + GetDeviceStatusDAOutput, + GetDeviceStatusDAInput, + GetDeviceStatusDAError, + GetDeviceStatusDAIntermediateValue + >, + { + title: "Go to dashboard", + description: "Navigate to the dashboard", + executeDeviceAction: (_, inspect) => { + const deviceAction = new GoToDashboardDeviceAction({ + input: { unlockTimeout: UNLOCK_TIMEOUT }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, + } satisfies DeviceActionProps< + GoToDashboardDAOutput, + GoToDashboardDAInput, + GoToDashboardDAError, + GoToDashboardDAIntermediateValue + >, + { + title: "List apps", + description: "List all applications installed on the device", + executeDeviceAction: (_, inspect) => { + const deviceAction = new ListAppsDeviceAction({ + input: { unlockTimeout: UNLOCK_TIMEOUT }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, + } satisfies DeviceActionProps< + ListAppsDAOutput, + ListAppsDAInput, + ListAppsDAError, + ListAppsDAIntermediateValue + >, + { + title: "List apps with metadata", + description: + "List all applications installed on the device with additional metadata", + executeDeviceAction: (_, inspect) => { + const deviceAction = new ListAppsWithMetadataDeviceAction({ + input: { unlockTimeout: UNLOCK_TIMEOUT }, + inspect, + }); + return sdk.executeDeviceAction({ + sessionId: selectedSessionId, + deviceAction, + }); + }, + initialValues: { unlockTimeout: UNLOCK_TIMEOUT }, + } satisfies DeviceActionProps< + ListAppsWithMetadataDAOutput, + ListAppsWithMetadataDAInput, + ListAppsWithMetadataDAError, + ListAppsWithMetadataDAIntermediateValue + >, + ], [], ); diff --git a/packages/core/_templates/core-module/with-prompt/di-module.ejs.t b/packages/core/_templates/core-module/with-prompt/di-module.ejs.t index e3e1e0587..c8f9e381b 100644 --- a/packages/core/_templates/core-module/with-prompt/di-module.ejs.t +++ b/packages/core/_templates/core-module/with-prompt/di-module.ejs.t @@ -7,7 +7,7 @@ import { types } from "./<%= moduleName %>Types"; type FactoryProps = {}; -const <%= moduleName %>ModuleFactory = ({}: Partial = {}) => +const <%= moduleName %>ModuleFactory = ({}: FactoryProps) => new ContainerModule((bind, _unbind, _isBound, _rebind, _unbindAsync, _onActivation, _onDeactivation) => { bind(types.<%= h.capitalize(moduleName) %>Service).to(Default<%= h.capitalize(moduleName) %>Service); }); diff --git a/packages/core/src/api/DeviceSdk.test.ts b/packages/core/src/api/DeviceSdk.test.ts index e5d05075c..ba3c6648f 100644 --- a/packages/core/src/api/DeviceSdk.test.ts +++ b/packages/core/src/api/DeviceSdk.test.ts @@ -20,7 +20,13 @@ describe("DeviceSdk", () => { describe("clean", () => { beforeEach(() => { logger = new ConsoleLogger(); - sdk = new DeviceSdk({ stub: false, loggers: [logger] }); + sdk = new DeviceSdk({ + stub: false, + loggers: [logger], + config: { + managerApiUrl: "http://fake.url", + }, + }); }); it("should create an instance", () => { @@ -59,7 +65,13 @@ describe("DeviceSdk", () => { describe("stubbed", () => { beforeEach(() => { - sdk = new DeviceSdk({ stub: true, loggers: [] }); + sdk = new DeviceSdk({ + stub: true, + loggers: [], + config: { + managerApiUrl: "http://fake.url", + }, + }); }); it("should create a stubbed sdk", () => { @@ -94,19 +106,4 @@ describe("DeviceSdk", () => { expect(uc.execute()).toBe("stub"); }); }); - - describe("without args", () => { - beforeEach(() => { - sdk = new DeviceSdk(); - }); - - it("should create an instance", () => { - expect(sdk).toBeDefined(); - expect(sdk).toBeInstanceOf(DeviceSdk); - }); - - it("should return a clean `version`", async () => { - expect(await sdk.getVersion()).toBe(pkg.version); - }); - }); }); diff --git a/packages/core/src/api/DeviceSdk.ts b/packages/core/src/api/DeviceSdk.ts index be75cc3df..47fa1ed64 100644 --- a/packages/core/src/api/DeviceSdk.ts +++ b/packages/core/src/api/DeviceSdk.ts @@ -53,7 +53,7 @@ import { SdkError } from "./Error"; export class DeviceSdk { readonly container: Container; /** @internal */ - constructor({ stub, loggers, config }: Partial = {}) { + constructor({ stub, loggers, config }: MakeContainerProps) { // NOTE: MakeContainerProps might not be the exact type here // For the init of the project this is sufficient, but we might need to // update the constructor arguments as we go (we might have more than just the container config) diff --git a/packages/core/src/api/device-action/__test-utils__/data.ts b/packages/core/src/api/device-action/__test-utils__/data.ts new file mode 100644 index 000000000..1b5e24e05 --- /dev/null +++ b/packages/core/src/api/device-action/__test-utils__/data.ts @@ -0,0 +1,108 @@ +import { AppType } from "@internal/manager-api/model/ManagerApiResponses"; + +export const BTC_APP = { + appEntryLength: 77, + appSizeInBlocks: 3227, + appCodeHash: + "924b5ba590971b3e98537cf8241f0aa51b1e6f26c37915dd38b83255168255d5", + appFullHash: + "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", + appName: "Bitcoin", +}; +export const BTC_APP_METADATA = { + versionId: 36248, + versionName: "Bitcoin", + versionDisplayName: "Bitcoin", + version: "2.2.2", + currencyId: "bitcoin", + description: "", + applicationType: AppType.currency, + dateModified: "2024-04-08T11:31:34.847313Z", + icon: "bitcoin", + authorName: " Ledger", + supportURL: + "https://support.ledger.com/hc/en-us/articles/115005195945-Bitcoin-BTC-", + contactURL: "mailto:https://support.ledger.com/hc/en-us/requests/new", + sourceURL: "https://github.com/LedgerHQ/app-bitcoin-new", + compatibleWallets: + '[ { "name": "Electrum", "url": "https://electrum.org/#home" } ]', + hash: "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", + perso: "perso_11", + firmware: "stax/1.4.0-rc2/bitcoin/app_2.2.2", + firmwareKey: "stax/1.4.0-rc2/bitcoin/app_2.2.2_key", + delete: "stax/1.4.0-rc2/bitcoin/app_2.2.2_del", + deleteKey: "stax/1.4.0-rc2/bitcoin/app_2.2.2_del_key", + bytes: 103264, + warning: null, + isDevTools: false, + category: 1, + parent: null, + parentName: null, +}; +export const CUSTOM_LOCK_SCREEN_APP = { + appEntryLength: 70, + appSizeInBlocks: 1093, + appCodeHash: + "0000000000000000000000000000000000000000000000000000000000000000", + appFullHash: + "5602b3d3fdde77fc02eb451a8beec4155bcf8b83ced794d7b3c63afaed5ff8c6", + appName: "", +}; +export const CUSTOM_LOCK_SCREEN_APP_METADATA = null; +export const ETH_APP = { + appEntryLength: 78, + appSizeInBlocks: 4120, + appCodeHash: + "4fdb751c0444f3a982c2ae9dcfde6ebe6dab03613d496f5e53cf91bce8ca46b5", + appFullHash: + "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", + appName: "Ethereum", +}; +export const ETH_APP_METADATA = { + versionId: 36185, + versionName: "Ethereum", + versionDisplayName: "Ethereum", + version: "1.10.4", + currencyId: "ethereum", + description: "", + applicationType: AppType.currency, + dateModified: "2024-04-09T12:28:55.783551Z", + icon: "ethereum", + authorName: " Ledger", + supportURL: + "https://support.ledger.com/hc/en-us/articles/360009576554-Ethereum-ETH-", + contactURL: "mailto:https://support.ledger.com/hc/en-us/requests/new", + sourceURL: "https://github.com/LedgerHQ/app-ethereum", + compatibleWallets: + '[ { "name": "Metamask", "url": "https://metamask.io/" }, { "name": "Phantom", "url": "https://phantom.app/" }, { "name": "Rabby", "url": "https://rabby.io/" }, { "name": "Rainbow", "url": "https://rainbow.me/" }, { "name": "MyCrypto", "url": "https://www.ledger.com/mycrypto/" }, { "name": "MyEtherWallet", "url": "https://www.ledger.com/myetherwallet/" } ]', + hash: "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", + perso: "perso_11", + firmware: "stax/1.4.0-rc3/ethereum/app_1.10.4", + firmwareKey: "stax/1.4.0-rc3/ethereum/app_1.10.4_key", + delete: "stax/1.4.0-rc3/ethereum/app_1.10.4_del", + deleteKey: "stax/1.4.0-rc3/ethereum/app_1.10.4_del_key", + bytes: 131852, + warning: "", + isDevTools: false, + category: 1, + parent: null, + parentName: null, +}; +export const SOLANA_APP = { + appEntryLength: 76, + appSizeInBlocks: 2568, + appCodeHash: + "dcc77e385de4394f579fa7b6eeb7293950fe5aec6d5355a7049f77bc0d02de24", + appFullHash: + "afbdaa67241e21c00191b177198615b50c98e5db998c3bba1d78093a85dbedee", + appName: "Solana", +}; +export const DOGECOIN_APP = { + appEntryLength: 78, + appSizeInBlocks: 2458, + appCodeHash: + "e59eee7bd32b1af2d93c5d8211e33d844d153a710d800254ea754e10ce18e7a9", + appFullHash: + "227001130f66297406696a19e1cf1e8e8b0cc14d5824ae8b1da98122c322e22e", + appName: "Dogecoin", +}; diff --git a/packages/core/src/api/device-action/__test-utils__/setupTestMachine.ts b/packages/core/src/api/device-action/__test-utils__/setupTestMachine.ts new file mode 100644 index 000000000..2e2b23a55 --- /dev/null +++ b/packages/core/src/api/device-action/__test-utils__/setupTestMachine.ts @@ -0,0 +1,109 @@ +import { Left, Right } from "purify-ts"; +import { assign, createMachine } from "xstate"; + +import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; +import { UnknownDAError } from "@api/device-action/os/Errors"; +import { GetDeviceStatusDeviceAction } from "@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction"; +import { GoToDashboardDeviceAction } from "@api/device-action/os/GoToDashboard/GoToDashboardDeviceAction"; +import { ListAppsDeviceAction } from "@api/device-action/os/ListApps/ListAppsDeviceAction"; +import { SdkError } from "@api/Error"; + +import { BTC_APP } from "./data"; + +type App = typeof BTC_APP; + +export const setupListAppsMock = (apps: App[], error = false) => { + (ListAppsDeviceAction as jest.Mock).mockImplementation(() => ({ + makeStateMachine: jest.fn().mockImplementation(() => + createMachine({ + id: "MockListAppsDeviceAction", + initial: "ready", + states: { + ready: { + after: { + 0: "done", + }, + entry: assign({ + intermediateValue: () => ({ + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }), + }), + }, + done: { + type: "final", + }, + }, + output: () => { + return error + ? Left(new UnknownDAError("ListApps failed")) + : Right(apps); + }, + }), + ), + })); +}; + +export const setupGoToDashboardMock = (error: boolean = false) => { + (GoToDashboardDeviceAction as jest.Mock).mockImplementation(() => ({ + makeStateMachine: jest.fn().mockImplementation(() => + createMachine({ + id: "MockGoToDashboardDeviceAction", + initial: "ready", + states: { + ready: { + after: { + 0: "done", + }, + entry: assign({ + intermediateValue: () => ({ + requiredUserInteraction: UserInteractionRequired.None, + }), + }), + }, + done: { + type: "final", + }, + }, + output: () => { + return error + ? Left(new UnknownDAError("GoToDashboard failed")) + : Right(undefined); + }, + }), + ), + })); +}; + +export const setupGetDeviceStatusMock = ( + output: { currentApp: string; currentAppVersion: string } | SdkError = { + currentApp: "BOLOS", + currentAppVersion: "1.0.0", + }, +) => { + (GetDeviceStatusDeviceAction as jest.Mock).mockImplementation(() => ({ + makeStateMachine: jest.fn().mockImplementation(() => + createMachine({ + id: "MockGetDeviceStatusDeviceAction", + initial: "ready", + states: { + ready: { + after: { + 0: "done", + }, + entry: assign({ + intermediateValue: () => ({ + requiredUserInteraction: UserInteractionRequired.None, + }), + }), + }, + done: { + type: "final", + }, + }, + output: () => { + return "currentApp" in output ? Right(output) : Left(output); + }, + }), + ), + })); +}; diff --git a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts index 61e7cee57..3c01af633 100644 --- a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts @@ -49,10 +49,8 @@ describe("GetDeviceStatusDeviceAction", () => { }); apiGetDeviceSessionStateMock.mockReturnValue({ - sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, + sessionStateType: DeviceSessionStateType.Connected, deviceStatus: DeviceStatus.CONNECTED, - currentApp: "mockedCurrentApp", - installedApps: [], }); sendCommandMock.mockResolvedValue({ diff --git a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts b/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts index 0c111cbf7..8d08a3e46 100644 --- a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts @@ -1,55 +1,17 @@ -import { Left, Right } from "purify-ts"; -import { assign, createMachine } from "xstate"; - import { DeviceStatus } from "@api/device/DeviceStatus"; import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { setupGetDeviceStatusMock } from "@api/device-action/__test-utils__/setupTestMachine"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; import { UnknownDAError } from "@api/device-action/os/Errors"; -import { GetDeviceStatusDeviceAction } from "@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction"; import { DeviceSessionStateType } from "@api/device-session/DeviceSessionState"; -import { SdkError } from "@api/Error"; import { GoToDashboardDeviceAction } from "./GoToDashboardDeviceAction"; import { GoToDashboardDAState } from "./types"; jest.mock("@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction"); -const setupGetDeviceStatusMock = ( - output: { currentApp: string; currentAppVersion: string } | SdkError = { - currentApp: "BOLOS", - currentAppVersion: "1.0.0", - }, -) => { - (GetDeviceStatusDeviceAction as jest.Mock).mockImplementation(() => ({ - makeStateMachine: jest.fn().mockImplementation(() => - createMachine({ - id: "MockGetDeviceStatusDeviceAction", - initial: "ready", - states: { - ready: { - after: { - 0: "done", - }, - entry: assign({ - intermediateValue: () => ({ - requiredUserInteraction: UserInteractionRequired.None, - }), - }), - }, - done: { - type: "final", - }, - }, - output: () => { - return "currentApp" in output ? Right(output) : Left(output); - }, - }), - ), - })); -}; - describe("GoToDashboardDeviceAction", () => { const closeAppMock = jest.fn(); const getAppAndVersionMock = jest.fn(); diff --git a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts b/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts index 2250c38c4..efa48f675 100644 --- a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts @@ -1,7 +1,13 @@ -import { Left, Right } from "purify-ts"; -import { assign, createMachine } from "xstate"; - +import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; +import { + BTC_APP, + CUSTOM_LOCK_SCREEN_APP, + DOGECOIN_APP, + ETH_APP, + SOLANA_APP, +} from "@api/device-action/__test-utils__/data"; import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { setupGoToDashboardMock } from "@api/device-action/__test-utils__/setupTestMachine"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; @@ -9,92 +15,12 @@ import { ListAppsRejectedError, UnknownDAError, } from "@api/device-action/os/Errors"; -import { GoToDashboardDeviceAction } from "@api/device-action/os/GoToDashboard/GoToDashboardDeviceAction"; import { ListAppsDeviceAction } from "./ListAppsDeviceAction"; import { ListAppsDAState } from "./types"; -const BTC_APP = { - appEntryLength: 77, - appSizeInBlocks: 3227, - appCodeHash: - "924b5ba590971b3e98537cf8241f0aa51b1e6f26c37915dd38b83255168255d5", - appFullHash: - "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", - appName: "Bitcoin", -}; -const CUSTOM_LOCK_SCREEN_APP = { - appEntryLength: 70, - appSizeInBlocks: 1093, - appCodeHash: - "0000000000000000000000000000000000000000000000000000000000000000", - appFullHash: - "5602b3d3fdde77fc02eb451a8beec4155bcf8b83ced794d7b3c63afaed5ff8c6", - appName: "", -}; - -const ETH_APP = { - appEntryLength: 78, - appSizeInBlocks: 4120, - appCodeHash: - "4fdb751c0444f3a982c2ae9dcfde6ebe6dab03613d496f5e53cf91bce8ca46b5", - appFullHash: - "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", - appName: "Ethereum", -}; - -const SOLANA_APP = { - appEntryLength: 76, - appSizeInBlocks: 2568, - appCodeHash: - "dcc77e385de4394f579fa7b6eeb7293950fe5aec6d5355a7049f77bc0d02de24", - appFullHash: - "afbdaa67241e21c00191b177198615b50c98e5db998c3bba1d78093a85dbedee", - appName: "Solana", -}; -const DOGECOIN_APP = { - appEntryLength: 78, - appSizeInBlocks: 2458, - appCodeHash: - "e59eee7bd32b1af2d93c5d8211e33d844d153a710d800254ea754e10ce18e7a9", - appFullHash: - "227001130f66297406696a19e1cf1e8e8b0cc14d5824ae8b1da98122c322e22e", - appName: "Dogecoin", -}; - jest.mock("@api/device-action/os/GoToDashboard/GoToDashboardDeviceAction"); -const setupGoToDashboardMock = (error: boolean = false) => { - (GoToDashboardDeviceAction as jest.Mock).mockImplementation(() => ({ - makeStateMachine: jest.fn().mockImplementation(() => - createMachine({ - id: "MockGoToDashboardDeviceAction", - initial: "ready", - states: { - ready: { - after: { - 0: "done", - }, - entry: assign({ - intermediateValue: () => ({ - requiredUserInteraction: UserInteractionRequired.None, - }), - }), - }, - done: { - type: "final", - }, - }, - output: () => { - return error - ? Left(new UnknownDAError("GoToDashboard failed")) - : Right(undefined); - }, - }), - ), - })); -}; - describe("ListAppsDeviceAction", () => { const { sendCommand: sendCommandMock } = makeInternalApiMock(); @@ -349,7 +275,6 @@ describe("ListAppsDeviceAction", () => { .mockResolvedValueOnce([BTC_APP, CUSTOM_LOCK_SCREEN_APP]) .mockResolvedValueOnce([ETH_APP, SOLANA_APP]) .mockResolvedValueOnce([DOGECOIN_APP]); - const expectedStates: Array = [ { intermediateValue: { @@ -388,7 +313,7 @@ describe("ListAppsDeviceAction", () => { ETH_APP, SOLANA_APP, DOGECOIN_APP, - ], + ] as ListAppsResponse, status: DeviceActionStatus.Completed, // Success }, ]; diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts index 0a949ab8c..d21c45f16 100644 --- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts @@ -1,146 +1,28 @@ import { Left, Right } from "purify-ts"; -import { assign, createMachine } from "xstate"; +import { DeviceStatus } from "@api/device/DeviceStatus"; +import { + BTC_APP, + BTC_APP_METADATA, + CUSTOM_LOCK_SCREEN_APP, + CUSTOM_LOCK_SCREEN_APP_METADATA, + ETH_APP, + ETH_APP_METADATA, +} from "@api/device-action/__test-utils__/data"; import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { setupListAppsMock } from "@api/device-action/__test-utils__/setupTestMachine"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; import { UnknownDAError } from "@api/device-action/os/Errors"; -import { ListAppsDeviceAction } from "@api/device-action/os/ListApps/ListAppsDeviceAction"; -import { AppType } from "@internal/manager-api/model/ManagerApiResponses"; +import { DeviceSessionStateType } from "@api/device-session/DeviceSessionState"; +import { FetchError } from "@internal/manager-api/model/Errors"; import { ListAppsWithMetadataDeviceAction } from "./ListAppsWithMetadataDeviceAction"; import { ListAppsWithMetadataDAState } from "./types"; jest.mock("@api/device-action/os/ListApps/ListAppsDeviceAction"); -const BTC_APP = { - appEntryLength: 77, - appSizeInBlocks: 3227, - appCodeHash: - "924b5ba590971b3e98537cf8241f0aa51b1e6f26c37915dd38b83255168255d5", - appFullHash: - "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", - appName: "Bitcoin", -}; - -const BTC_APP_METADATA = { - versionId: 36248, - versionName: "Bitcoin", - versionDisplayName: "Bitcoin", - version: "2.2.2", - currencyId: "bitcoin", - description: "", - applicationType: AppType.currency, - dateModified: "2024-04-08T11:31:34.847313Z", - icon: "bitcoin", - authorName: " Ledger", - supportURL: - "https://support.ledger.com/hc/en-us/articles/115005195945-Bitcoin-BTC-", - contactURL: "mailto:https://support.ledger.com/hc/en-us/requests/new", - sourceURL: "https://github.com/LedgerHQ/app-bitcoin-new", - compatibleWallets: - '[ { "name": "Electrum", "url": "https://electrum.org/#home" } ]', - hash: "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", - perso: "perso_11", - firmware: "stax/1.4.0-rc2/bitcoin/app_2.2.2", - firmwareKey: "stax/1.4.0-rc2/bitcoin/app_2.2.2_key", - delete: "stax/1.4.0-rc2/bitcoin/app_2.2.2_del", - deleteKey: "stax/1.4.0-rc2/bitcoin/app_2.2.2_del_key", - bytes: 103264, - warning: null, - isDevTools: false, - category: 1, - parent: null, - parentName: null, -}; - -// const CUSTOM_LOCK_SCREEN_APP = { -// appEntryLength: 70, -// appSizeInBlocks: 1093, -// appCodeHash: -// "0000000000000000000000000000000000000000000000000000000000000000", -// appFullHash: -// "5602b3d3fdde77fc02eb451a8beec4155bcf8b83ced794d7b3c63afaed5ff8c6", -// appName: "", -// }; - -// const CUSTOM_LOCK_SCREEN_APP_METADATA = null; - -// const ETH_APP = { -// appEntryLength: 78, -// appSizeInBlocks: 4120, -// appCodeHash: -// "4fdb751c0444f3a982c2ae9dcfde6ebe6dab03613d496f5e53cf91bce8ca46b5", -// appFullHash: -// "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", -// appName: "Ethereum", -// }; - -// const ETH_APP_METADATA = { -// versionId: 36185, -// versionName: "Ethereum", -// versionDisplayName: "Ethereum", -// version: "1.10.4", -// currencyId: "ethereum", -// description: "", -// applicationType: AppType.currency, -// dateModified: "2024-04-09T12:28:55.783551Z", -// icon: "ethereum", -// authorName: " Ledger", -// supportURL: -// "https://support.ledger.com/hc/en-us/articles/360009576554-Ethereum-ETH-", -// contactURL: "mailto:https://support.ledger.com/hc/en-us/requests/new", -// sourceURL: "https://github.com/LedgerHQ/app-ethereum", -// compatibleWallets: -// '[ { "name": "Metamask", "url": "https://metamask.io/" }, { "name": "Phantom", "url": "https://phantom.app/" }, { "name": "Rabby", "url": "https://rabby.io/" }, { "name": "Rainbow", "url": "https://rainbow.me/" }, { "name": "MyCrypto", "url": "https://www.ledger.com/mycrypto/" }, { "name": "MyEtherWallet", "url": "https://www.ledger.com/myetherwallet/" } ]', -// hash: "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", -// perso: "perso_11", -// firmware: "stax/1.4.0-rc3/ethereum/app_1.10.4", -// firmwareKey: "stax/1.4.0-rc3/ethereum/app_1.10.4_key", -// delete: "stax/1.4.0-rc3/ethereum/app_1.10.4_del", -// deleteKey: "stax/1.4.0-rc3/ethereum/app_1.10.4_del_key", -// bytes: 131852, -// warning: "", -// isDevTools: false, -// category: 1, -// parent: null, -// parentName: null, -// }; - -type App = typeof BTC_APP; - -const setupListAppsMock = (apps: App[], error = false) => { - (ListAppsDeviceAction as jest.Mock).mockImplementation(() => ({ - makeStateMachine: jest.fn().mockImplementation(() => - createMachine({ - id: "MockListAppsDeviceAction", - initial: "ready", - states: { - ready: { - after: { - 0: "done", - }, - entry: assign({ - intermediateValue: () => ({ - requiredUserInteraction: UserInteractionRequired.AllowListApps, - }), - }), - }, - done: { - type: "final", - }, - }, - output: () => { - return error - ? Left(new UnknownDAError("ListApps failed")) - : Right(apps); - }, - }), - ), - })); -}; - describe("ListAppsWithMetadataDeviceAction", () => { const { managerApiService: managerApiServiceMock, @@ -148,6 +30,18 @@ describe("ListAppsWithMetadataDeviceAction", () => { // setDeviceSessionState: apiSetDeviceSessionStateMock, } = makeInternalApiMock(); + const saveSessionStateMock = jest.fn(); + const getDeviceSessionStateMock = jest.fn(); + const getAppsByHashMock = jest.fn(); + + function extractDependenciesMock() { + return { + getAppsByHash: getAppsByHashMock, + getDeviceSessionState: getDeviceSessionStateMock, + saveSessionState: saveSessionStateMock, + }; + } + beforeEach(() => { jest.resetAllMocks(); }); @@ -160,7 +54,9 @@ describe("ListAppsWithMetadataDeviceAction", () => { input: {}, }); - jest.spyOn(managerApiServiceMock, "getAppsByHash").mockResolvedValue([]); + jest + .spyOn(managerApiServiceMock, "getAppsByHash") + .mockResolvedValue(Right([])); const expectedStates: Array = [ { @@ -198,7 +94,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { jest .spyOn(managerApiServiceMock, "getAppsByHash") - .mockResolvedValue([BTC_APP_METADATA]); + .mockResolvedValue(Right([BTC_APP_METADATA])); const expectedStates: Array = [ { @@ -217,13 +113,13 @@ describe("ListAppsWithMetadataDeviceAction", () => { intermediateValue: { requiredUserInteraction: UserInteractionRequired.None, }, - status: DeviceActionStatus.Pending, // ListAppsChecks + status: DeviceActionStatus.Pending, // FetchMetadata }, { intermediateValue: { requiredUserInteraction: UserInteractionRequired.None, }, - status: DeviceActionStatus.Pending, // Success + status: DeviceActionStatus.Pending, // SaveSession }, { status: DeviceActionStatus.Completed, @@ -238,7 +134,298 @@ describe("ListAppsWithMetadataDeviceAction", () => { done, ); }); + + it("should run the device actions with 2 apps installed", (done) => { + setupListAppsMock([BTC_APP, ETH_APP]); + const listAppsWithMetadataDeviceAction = + new ListAppsWithMetadataDeviceAction({ + input: {}, + }); + + jest + .spyOn(managerApiServiceMock, "getAppsByHash") + .mockResolvedValue(Right([BTC_APP_METADATA, ETH_APP_METADATA])); + + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Ready + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }, + status: DeviceActionStatus.Pending, // ListAppsDeviceAction + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // FetchMetadata + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // SaveSession + }, + { + status: DeviceActionStatus.Completed, + output: [BTC_APP_METADATA, ETH_APP_METADATA], + }, + ]; + + testDeviceActionStates( + listAppsWithMetadataDeviceAction, + expectedStates, + makeInternalApiMock(), + done, + ); + }); + + it("should run the device actions with 1 app installed and a custom lock screen", (done) => { + setupListAppsMock([BTC_APP, CUSTOM_LOCK_SCREEN_APP]); + const listAppsWithMetadataDeviceAction = + new ListAppsWithMetadataDeviceAction({ + input: {}, + }); + + jest + .spyOn(managerApiServiceMock, "getAppsByHash") + .mockResolvedValue( + Right([BTC_APP_METADATA, CUSTOM_LOCK_SCREEN_APP_METADATA]), + ); + + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Ready + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }, + status: DeviceActionStatus.Pending, // ListAppsDeviceAction + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // FetchMetadata + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // SaveSession + }, + { + status: DeviceActionStatus.Completed, + output: [BTC_APP_METADATA, CUSTOM_LOCK_SCREEN_APP_METADATA], + }, + ]; + + testDeviceActionStates( + listAppsWithMetadataDeviceAction, + expectedStates, + makeInternalApiMock(), + done, + ); + }); }); - // TODO: finish testing + describe("error case", () => { + it("should error when ListApps fails", (done) => { + setupListAppsMock([], true); + const listAppsWithMetadataDeviceAction = + new ListAppsWithMetadataDeviceAction({ + input: {}, + }); + + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Ready + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }, + status: DeviceActionStatus.Pending, // ListAppsDeviceAction + }, + { + status: DeviceActionStatus.Error, + error: new UnknownDAError("ListApps failed"), + }, + ]; + + testDeviceActionStates( + listAppsWithMetadataDeviceAction, + expectedStates, + makeInternalApiMock(), + done, + ); + }); + + it("should error when getAppsByHash rejects", (done) => { + setupListAppsMock([BTC_APP]); + const listAppsWithMetadataDeviceAction = + new ListAppsWithMetadataDeviceAction({ + input: {}, + }); + + jest + .spyOn(managerApiServiceMock, "getAppsByHash") + .mockRejectedValue(new UnknownDAError("getAppsByHash failed")); + + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Ready + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }, + status: DeviceActionStatus.Pending, // ListAppsDeviceAction + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // FetchMetadata + }, + { + status: DeviceActionStatus.Error, + error: new UnknownDAError("getAppsByHash failed"), + }, + ]; + + testDeviceActionStates( + listAppsWithMetadataDeviceAction, + expectedStates, + makeInternalApiMock(), + done, + ); + }); + + it("should error when getAppsByHash fails but error is known", (done) => { + setupListAppsMock([BTC_APP]); + const listAppsWithMetadataDeviceAction = + new ListAppsWithMetadataDeviceAction({ + input: {}, + }); + + const error = new FetchError(new Error("Failed to fetch data")); + + jest + .spyOn(managerApiServiceMock, "getAppsByHash") + .mockResolvedValue(Left(error)); + + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Ready + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }, + status: DeviceActionStatus.Pending, // ListAppsDeviceAction + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // FetchMetadata + }, + { + status: DeviceActionStatus.Error, + error, + }, + ]; + + testDeviceActionStates( + listAppsWithMetadataDeviceAction, + expectedStates, + makeInternalApiMock(), + done, + ); + }); + + it("should error when SaveSession fails", (done) => { + setupListAppsMock([BTC_APP]); + const listAppsWithMetadataDeviceAction = + new ListAppsWithMetadataDeviceAction({ + input: {}, + }); + + getAppsByHashMock.mockImplementation(async () => + Promise.resolve(Right([BTC_APP_METADATA])), + ); + + jest + .spyOn(listAppsWithMetadataDeviceAction, "extractDependencies") + .mockReturnValue(extractDependenciesMock()); + + getDeviceSessionStateMock.mockReturnValue({ + sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, + deviceStatus: DeviceStatus.CONNECTED, + currentApp: "BOLOS", + installedApps: [], + }); + + saveSessionStateMock.mockImplementation(() => { + throw new UnknownDAError("SaveSession failed"); + }); + + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // Ready + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.AllowListApps, + }, + status: DeviceActionStatus.Pending, // ListAppsDeviceAction + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // FetchMetadata + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, // SaveSession + }, + { + status: DeviceActionStatus.Error, + error: new UnknownDAError("SaveSession failed"), + }, + ]; + + testDeviceActionStates( + listAppsWithMetadataDeviceAction, + expectedStates, + makeInternalApiMock(), + done, + ); + }); + }); }); diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts index f078a0620..909a2e6d5 100644 --- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts @@ -1,4 +1,4 @@ -import { Left, Right } from "purify-ts"; +import { EitherAsync, Left, Right } from "purify-ts"; import { AnyEventObject, assign, @@ -10,12 +10,13 @@ import { import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; import { InternalApi } from "@api/device-action/DeviceAction"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; -import { UnknownDAError } from "@api/device-action/os/Errors"; +import { DEFAULT_UNLOCK_TIMEOUT_MS } from "@api/device-action/os/Const"; import { ListAppsDeviceAction } from "@api/device-action/os/ListApps/ListAppsDeviceAction"; import { ListAppsDAOutput } from "@api/device-action/os/ListApps/types"; import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes"; import { XStateDeviceAction } from "@api/device-action/xstate-utils/XStateDeviceAction"; import { DeviceSessionState } from "@api/device-session/DeviceSessionState"; +import { FetchError } from "@internal/manager-api/model/Errors"; import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; import { @@ -36,7 +37,7 @@ export type MachineDependencies = { input, }: { input: ListAppsDAOutput; - }) => Promise; + }) => EitherAsync>; getDeviceSessionState: () => DeviceSessionState; saveSessionState: (state: DeviceSessionState) => DeviceSessionState; }; @@ -60,7 +61,7 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< const { getAppsByHash, saveSessionState, getDeviceSessionState } = this.extractDependencies(internalAPI); - const unlockTimeout = this.input.unlockTimeout ?? 0; + const unlockTimeout = this.input.unlockTimeout ?? DEFAULT_UNLOCK_TIMEOUT_MS; const listAppsMachine = new ListAppsDeviceAction({ input: { @@ -78,9 +79,7 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< }, actors: { listApps: listAppsMachine, - getAppsByHash: fromPromise( - getAppsByHash, - ), + getAppsByHash: fromPromise(getAppsByHash), saveSessionState: fromCallback( ({ input, @@ -88,21 +87,24 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< }: { sendBack: (event: AnyEventObject) => void; input: { - appsWithMetadata: ApplicationEntity[]; + appsWithMetadata: Array; }; }) => { const { appsWithMetadata } = input; - if (!appsWithMetadata) { - return sendBack({ type: "error" }); - } + + const filterted = appsWithMetadata.filter((app) => app !== null); const sessionState = getDeviceSessionState(); const updatedState = { ...sessionState, - installedApps: appsWithMetadata, + installedApps: filterted, }; - saveSessionState(updatedState); - sendBack({ type: "done" }); + try { + saveSessionState(updatedState); + sendBack({ type: "done" }); + } catch (error) { + sendBack({ type: "error", error }); + } }, ), }, @@ -120,15 +122,9 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< error: _.event["error"], // FIXME: add a typeguard }), }), - assignErrorSaveAppState: assign({ - _internalState: (_) => ({ - ..._.context._internalState, - error: new UnknownDAError("SaveSession Error"), - }), - }), }, }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5QBkCWsAuBBADj2A6qhgBYCyYGAhhFdQCJgBuqAxmFqxqgPYB2AOkYt2AJTA0AngGIA2gAYAuolA4esYrz4qQAD0QBGAJzyBADgBsAdgNX5Fow6NWjAGhCTDBswKsAmCwBWPwAWPwBmM0DjAwBfWPc0TFx8IlIKaloGZjYOLi0BJOw8WGkIfjABVD4mHgBrSoAbdGL8BWUkEDUNbn4dfQRgnyM-b0D5Kwt5EKsbd08EUKsBKYNAswn7eTMrMz94xJaUwmJySho6KmFczl7BIuPpMAAnZ55ngRxGugAzd4BbATNZIldo6bqaPqdAZDAQjMYTKYzOYeRBmAwCEJGbEWCzo+QGeQTA4gB4lNJnTKXa7sW4FMn4ADCJDArDqciU4PUkO00MQRgMGJCZhC4XCuMmAXk4XmiHWgUx9j8IqigUCeLiCVJR3JpwyF2yIjyd0KOqZLLZcgMHVU3Lu-X5gsxIrFEosUplqIQFnCQvsvuiyvsFhCJIZJ3S5yyVxytPy-FNIPNrPZsj8Nq6dq0DoQAqFLvF1ndU09CwCGLs4W2gRC42xzisYbNEcpBpjRrpCYAYpRWK3o2UKlUavVKjBWrAAEKSAASVFgJDBnQh9r5CDW8gVPr8diJBPGQVlCEiFelBnCJjMF4F+y14Yp+ujNONBR7GD7j8uT1e70+3wwfzPIC47HNOc4Lkuto9Nma4blu4Q7nu+72IER5+BEvgBIE4RWMKjhWIERjhE2SYtp+ho3PGggAMpUEwYDUXAGj8IOfBgJBmbQVCoADMYNYCIEsyWMEwZFkeITuoq25BKMZiWLehykQ+UbUrGL4JrR9GMbAzF8N+bzPBxK4wTxhhGPxgm7DJomTEe4oKuZvrbAYDh+NEazxFqfA8BAcA6PeeoqRRcarlBPI5gAtBYR5RQISHhBJB4mOeRgkROylUsF6mCM+4hSFyXG8qZCBhEegrLP4QSirYiHoqld7NhlbbPp29zNgV4VrsqDnYYSV4GH48joX4R5GCEAjoUEwRBOKGwhKGDVKYFmXtpRJrhsyKYdaFeiIFY4rmLhziipYU1uF6ARGAJuyEv4xiTO6aXHE1T5qa1Ahvh+QVUNtJm7euLnhJi2IiiMRLhNNaEHbWvoRGKCFqj6T26pGK0tVRAiaQxTF-cZ3H-bYBjjWeRNVg4FhE3ZGwCLYzgI-I5n7bYyOpMtzVvRj1EAK6sOwOm-fjAxwwqwSRP4bmilYR7RBYNOzc4uJquemqKelbOvR2GMAKI-s8AtFf9sl+AJvqEuKY12DsaFYnF2HVrWREBJEnmxEAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QBkCWsAuBBADj2A6qhgBYCyYGAhhFdQCJgBuqAxmFqxqgPYB2AOkYt2AJTA0AngGIA2gAYAuolA4esYrz4qQAD0QBGAJzyBADgBsAdgNWAzPIAs8+TceOANCEmILAJjMBKwsHOyMjAzN7SIBfGK80TFx8IlIKaloGZjYOLi0BROw8WGkIfjABVD4mHgBrCoAbdCL8BWUkEDUNbn4dfQQAVgCBIz9IgacQ6z8-Lx8Ef1N5OwsBlbs7dyNHCziE5uTCYnJKGjoqYRzOHsFCw+kwACdHnkeBHAa6ADNXgFsBJpJYptHRdTS9Dr9IaBUbjSYrKwzOaIMwGAQTFx2MxmOx+AYDKxGAZ7EB3YqpE4Zc6XdjXfJk-AAYRIYFYtTkSlB6nB2khKKGAj89jCRjCFmWdmRCD8LiCGIMMysqIsCpJDKOaVOmQu2VpeX4BQOxWZrPZsgM7VU3JufX5fkFwvCYolUqVjkFyxlA3FeO9VjVRpSx3SZyyIlyN0NQKZLLZcj8ls61q0toQZgFQo2TqM4o2UosjjsQRC8iFa1GfiJAejGspoZ14bpBoAYpRWHXtaVypVqnUKjAWrAAEKSAASVFgJBBHTBNr5CAM7gsAkcY3TZmW-iMiPzm0Flkc2PX8gMTmrg4pIe1NIj+VbGHbV-OD2er3enwwP0e-wHhxH48nacrW6FN50XHYVzXAYNxWSsd28RA7GMQUMUcaCLFFTZdniUlA1rJ8wyufVBHvR8tXOE04yApMQIhUB+kiVYBFLTYbBmPwCwsKUdjRRxghVMwAlWSIjHPQ5L3Iwi9UjUiOwo2MzQtLlaN5ejDEsAZmL8ViFRmTj8wLSDt3kbFtzGZYxPJYNJIbIjIwAZSoJgwHsuANH4Ls+DAajZ1AtTpQ4wJnH8UyHB9fNgiMjCzCdcUzEcSyg01KkpNvA1HOc1zYHcvgXxeR4fOTOi9EQPSgvkEKYrC0spQJIwRn4pw8QLAwQjiHC+B4CA4B0dUJJS2zpL8mieVTABaLiEIQMbNPCOb5vmyxEvwmybybQQb3EKRlNG+dV3zIVizCxwc3sPxlv6+s1uIqNBx2ud-ICWa1lPexsWMb18ycQUS3FfEFTsb0LusgbrsjdVKNqe7hv6CwogECwCxPQGhSsNCjHzVdmIMMKBgMBV8eMYHkqu3U0pIts5OoaHioY093QCEwMNFJV5Gg-NInRHGKoqnNSzR4mqcG8mBFkgiqEhmnVJKhdVndQTlncKxXCsfF829H6HFsEJvSFbD9hrS7rzJ9aBAyly3OG3zadK-nzAGUUzKsQlxQxqaQnq2xt1XNnnBMuxBfFsH8nsgBXVh2GyqXUzxCqBEB7YxlXGLIisV0cy5sLlcR53RUD1aTZugBRV9Hmj+dY+XBPV0XRnU9qyxzGiiqNwmOxgnamIgA */ id: "ListAppsWithMetadataDeviceAction", initial: "DeviceReady", context: (_) => { @@ -185,14 +181,7 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< }, onError: { target: "Error", - actions: [ - "assignErrorFromEvent", - assign({ - intermediateValue: (_) => ({ - requiredUserInteraction: UserInteractionRequired.None, - }), - }), - ], + entry: "assignErrorFromEvent", }, }, }, @@ -225,13 +214,19 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< src: "getAppsByHash", input: (_) => _.context._internalState.apps, onDone: { - target: "SaveSession", + target: "FetchMetadataCheck", actions: assign({ _internalState: (_) => { - return { - ..._.context._internalState, - appsWithMetadata: _.event.output, - }; + return _.event.output.caseOf({ + Right: (appsWithMetadata) => ({ + ..._.context._internalState, + appsWithMetadata, + }), + Left: (error) => ({ + ..._.context._internalState, + error, + }), + }); }, }), }, @@ -241,6 +236,17 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< }, }, }, + FetchMetadataCheck: { + always: [ + { + target: "Error", + guard: "hasError", + }, + { + target: "SaveSession", + }, + ], + }, SaveSession: { invoke: { src: "saveSessionState", @@ -277,10 +283,8 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< extractDependencies(internalApi: InternalApi): MachineDependencies { return { - getAppsByHash: async ({ input }) => { - const res = await internalApi.managerApiService.getAppsByHash(input); - return res; - }, + getAppsByHash: ({ input }) => + internalApi.managerApiService.getAppsByHash(input), getDeviceSessionState: () => internalApi.getDeviceSessionState(), saveSessionState: (state: DeviceSessionState) => internalApi.setDeviceSessionState(state), diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts index 91d93065d..23a9505dd 100644 --- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts @@ -9,7 +9,7 @@ import { import { SdkError } from "@api/Error"; import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; -export type ListAppsWithMetadataDAOutput = ApplicationEntity[]; +export type ListAppsWithMetadataDAOutput = Array; export type ListAppsWithMetadataDAInput = ListAppsDAInput; export type ListAppsWithMetadataDAError = diff --git a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts b/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts index 662864da7..b52e05e28 100644 --- a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts @@ -3,8 +3,8 @@ import { assign, createMachine } from "xstate"; import { Apdu } from "@api/apdu/model/Apdu"; import { ApduBuilder } from "@api/apdu/utils/ApduBuilder"; +import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; -import { InternalApi } from "@api/device-action/DeviceAction"; import { DeviceActionState, DeviceActionStatus, @@ -67,15 +67,7 @@ describe("SendCommandInAppDeviceAction", () => { sendCommand: sendMyCommand, }); - const apiSendCommandMock = jest.fn(); - const apiGetDeviceSessionStateMock = jest.fn(); - - const internalApiMock = (): InternalApi => ({ - sendCommand: apiSendCommandMock, - getDeviceSessionState: apiGetDeviceSessionStateMock, - getDeviceSessionStateObservable: jest.fn(), - setDeviceSessionState: jest.fn(), - }); + const { sendCommand: apiSendCommandMock } = makeInternalApiMock(); const commandParams = { paramString: "aParameter", @@ -102,7 +94,7 @@ describe("SendCommandInAppDeviceAction", () => { }, }); await new Promise((resolve, reject) => { - deviceAction._execute(internalApiMock()).observable.subscribe({ + deviceAction._execute(makeInternalApiMock()).observable.subscribe({ error: () => reject(), complete: () => resolve(), next: () => {}, @@ -147,7 +139,7 @@ describe("SendCommandInAppDeviceAction", () => { }, }), expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -197,7 +189,7 @@ describe("SendCommandInAppDeviceAction", () => { testDeviceActionStates( deviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); @@ -249,7 +241,7 @@ describe("SendCommandInAppDeviceAction", () => { testDeviceActionStates( deviceAction, expectedStates, - internalApiMock(), + makeInternalApiMock(), done, ); }); diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index 2b37f3447..b59bfabad 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -12,6 +12,7 @@ import { deviceSessionModuleFactory } from "@internal/device-session/di/deviceSe import { discoveryModuleFactory } from "@internal/discovery/di/discoveryModule"; import { loggerModuleFactory } from "@internal/logger-publisher/di/loggerModule"; import { managerApiModuleFactory } from "@internal/manager-api/di/managerApiModule"; +import { MANAGER_API_BASE_URL } from "@internal/manager-api/model/Const"; import { sendModuleFactory } from "@internal/send/di/sendModule"; import { usbModuleFactory } from "@internal/usb/di/usbModule"; @@ -19,16 +20,16 @@ import { usbModuleFactory } from "@internal/usb/di/usbModule"; // const logger = makeLoggerMiddleware(); export type MakeContainerProps = { - stub: boolean; - loggers: LoggerSubscriberService[]; + stub?: boolean; + loggers?: LoggerSubscriberService[]; config: SdkConfig; }; export const makeContainer = ({ stub = false, loggers = [], - config, -}: Partial) => { + config = { managerApiUrl: MANAGER_API_BASE_URL }, +}: MakeContainerProps) => { const container = new Container(); // Uncomment this line to enable the logger middleware diff --git a/packages/core/src/internal/config/di/configModule.test.ts b/packages/core/src/internal/config/di/configModule.test.ts index 2633454ad..8b5d25c0c 100644 --- a/packages/core/src/internal/config/di/configModule.test.ts +++ b/packages/core/src/internal/config/di/configModule.test.ts @@ -13,7 +13,7 @@ describe("configModuleFactory", () => { let container: Container; let mod: ReturnType; beforeEach(() => { - mod = configModuleFactory(); + mod = configModuleFactory({ stub: false }); container = new Container(); container.load(mod); }); diff --git a/packages/core/src/internal/config/di/configModule.ts b/packages/core/src/internal/config/di/configModule.ts index 762c9910d..e749a2a52 100644 --- a/packages/core/src/internal/config/di/configModule.ts +++ b/packages/core/src/internal/config/di/configModule.ts @@ -15,9 +15,7 @@ type FactoryProps = { stub: boolean; }; -export const configModuleFactory = ({ - stub = false, -}: Partial = {}) => +export const configModuleFactory = ({ stub }: FactoryProps) => new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(configTypes.LocalConfigDataSource).to(FileLocalConfigDataSource); bind(configTypes.RemoteConfigDataSource).to(RestRemoteConfigDataSource); diff --git a/packages/core/src/internal/device-model/di/deviceModelModule.test.ts b/packages/core/src/internal/device-model/di/deviceModelModule.test.ts index e3d149431..86301bc64 100644 --- a/packages/core/src/internal/device-model/di/deviceModelModule.test.ts +++ b/packages/core/src/internal/device-model/di/deviceModelModule.test.ts @@ -9,7 +9,7 @@ describe("deviceModelModuleFactory", () => { let container: Container; let mod: ReturnType; beforeEach(() => { - mod = deviceModelModuleFactory(); + mod = deviceModelModuleFactory({ stub: false }); container = new Container(); container.load(mod); }); diff --git a/packages/core/src/internal/device-model/di/deviceModelModule.ts b/packages/core/src/internal/device-model/di/deviceModelModule.ts index f41abd4ca..2d9d6ffed 100644 --- a/packages/core/src/internal/device-model/di/deviceModelModule.ts +++ b/packages/core/src/internal/device-model/di/deviceModelModule.ts @@ -8,9 +8,7 @@ type FactoryProps = { stub: boolean; }; -export const deviceModelModuleFactory = ({ - stub = false, -}: Partial = {}) => +export const deviceModelModuleFactory = ({ stub }: FactoryProps) => new ContainerModule((bind, _unbind, _isBound, _rebind) => { bind(deviceModelTypes.DeviceModelDataSource).to( StaticDeviceModelDataSource, diff --git a/packages/core/src/internal/device-session/model/DeviceSession.ts b/packages/core/src/internal/device-session/model/DeviceSession.ts index e78515b6d..84e066a5d 100644 --- a/packages/core/src/internal/device-session/model/DeviceSession.ts +++ b/packages/core/src/internal/device-session/model/DeviceSession.ts @@ -1,4 +1,3 @@ -// import { inject } from "inversify"; import { BehaviorSubject } from "rxjs"; import { v4 as uuidv4 } from "uuid"; @@ -57,12 +56,10 @@ export class DeviceSession { isPolling: true, triggersDisconnection: false, }), - updateStateFn: (fn) => { + updateStateFn: (callback) => { const state = this._deviceState.getValue(); - this.setDeviceSessionState(fn(state)); + this.setDeviceSessionState(callback(state)); }, - // updateStateFn: (state: DeviceSessionState) => - // this.setDeviceSessionState(state), }, loggerModuleFactory("device-session-refresher"), ); diff --git a/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts b/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts index 33394fda5..7d3243c2c 100644 --- a/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts +++ b/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts @@ -37,9 +37,12 @@ export type DeviceSessionRefresherArgs = { /** * Callback that updates the state of the device session with * polling response. - * @param state - The new state to update to. + * @param callback - A function that will take the previous state and return the new state. + * @returns void */ - updateStateFn(fn: (state: DeviceSessionState) => DeviceSessionState): void; + updateStateFn( + callback: (state: DeviceSessionState) => DeviceSessionState, + ): void; }; /** diff --git a/packages/core/src/internal/discovery/di/discoveryModule.test.ts b/packages/core/src/internal/discovery/di/discoveryModule.test.ts index 500ed6c21..a0c7b5575 100644 --- a/packages/core/src/internal/discovery/di/discoveryModule.test.ts +++ b/packages/core/src/internal/discovery/di/discoveryModule.test.ts @@ -17,14 +17,14 @@ describe("discoveryModuleFactory", () => { let container: Container; let mod: ReturnType; beforeEach(() => { - mod = discoveryModuleFactory(); + mod = discoveryModuleFactory({ stub: false }); container = new Container(); container.load( mod, // The following modules are injected into discovery module loggerModuleFactory(), - usbModuleFactory(), - deviceModelModuleFactory(), + usbModuleFactory({ stub: false }), + deviceModelModuleFactory({ stub: false }), deviceSessionModuleFactory(), managerApiModuleFactory({ config: { managerApiUrl: "http://fake.url" }, diff --git a/packages/core/src/internal/discovery/di/discoveryModule.ts b/packages/core/src/internal/discovery/di/discoveryModule.ts index 97d7bf25a..e2beda61b 100644 --- a/packages/core/src/internal/discovery/di/discoveryModule.ts +++ b/packages/core/src/internal/discovery/di/discoveryModule.ts @@ -12,9 +12,7 @@ type FactoryProps = { stub: boolean; }; -export const discoveryModuleFactory = ({ - stub = false, -}: Partial = {}) => +export const discoveryModuleFactory = ({ stub = false }: FactoryProps) => new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(discoveryTypes.ConnectUseCase).to(ConnectUseCase); bind(discoveryTypes.DisconnectUseCase).to(DisconnectUseCase); diff --git a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts index 32b315319..58bbd32a0 100644 --- a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts +++ b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts @@ -1,23 +1,92 @@ +import axios from "axios"; +import { Left, Right } from "purify-ts"; + +import { + BTC_APP, + BTC_APP_METADATA, + CUSTOM_LOCK_SCREEN_APP, + CUSTOM_LOCK_SCREEN_APP_METADATA, +} from "@api/device-action/__test-utils__/data"; import { MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const"; +import { FetchError } from "@internal/manager-api/model/Errors"; import { DefaultManagerApiDataSource } from "./DefaultManagerApiDataSource"; +jest.mock("axios"); + describe("DefaultManagerApiDataSource", () => { - it("fetch data", async () => { - const api = new DefaultManagerApiDataSource({ - managerApiUrl: MANAGER_API_BASE_URL, + describe("getAppsByHash", () => { + describe("success cases", () => { + it("with BTC app, should return the metadata", async () => { + const api = new DefaultManagerApiDataSource({ + managerApiUrl: MANAGER_API_BASE_URL, + }); + + jest.spyOn(axios, "post").mockResolvedValue({ + data: [BTC_APP_METADATA], + }); + + const hashes = [BTC_APP.appFullHash]; + + const apps = await api.getAppsByHash(hashes); + + expect(apps).toEqual(Right([BTC_APP_METADATA])); + }); + + it("with no apps, should return an empty list", async () => { + const api = new DefaultManagerApiDataSource({ + managerApiUrl: MANAGER_API_BASE_URL, + }); + + jest.spyOn(axios, "post").mockResolvedValue({ + data: [], + }); + + const hashes: string[] = []; + + const apps = await api.getAppsByHash(hashes); + + expect(apps).toEqual(Right([])); + }); + + it("with BTC app and custom lock screen, should return the metadata", async () => { + const api = new DefaultManagerApiDataSource({ + managerApiUrl: MANAGER_API_BASE_URL, + }); + + jest.spyOn(axios, "post").mockResolvedValue({ + data: [BTC_APP_METADATA, CUSTOM_LOCK_SCREEN_APP_METADATA], + }); + + const hashes = [ + BTC_APP.appFullHash, + CUSTOM_LOCK_SCREEN_APP.appFullHash, + ]; + + const apps = await api.getAppsByHash(hashes); + + expect(apps).toEqual( + Right([BTC_APP_METADATA, CUSTOM_LOCK_SCREEN_APP_METADATA]), + ); + }); }); - // appFullHash from the ListApps/ListAppsContinue command's response - const hashes = [ - "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38", - ]; + describe("error cases", () => { + it("should throw an error if the request fails", async () => { + const api = new DefaultManagerApiDataSource({ + managerApiUrl: MANAGER_API_BASE_URL, + }); + const err = new Error("fetch error"); + jest.spyOn(axios, "post").mockRejectedValue(err); - const apps = await api.getAppsByHash(hashes); - console.log(apps); + const hashes = [BTC_APP.appFullHash]; - expect(apps).toHaveLength(1); + try { + await api.getAppsByHash(hashes); + } catch (error) { + expect(error).toEqual(Left(new FetchError(err))); + } + }); + }); }); - - // TODO: finish testing }); diff --git a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts index 317c9d69e..b4e7f521f 100644 --- a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts +++ b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts @@ -1,8 +1,10 @@ import axios from "axios"; import { inject, injectable } from "inversify"; +import { EitherAsync } from "purify-ts"; import { type SdkConfig } from "@api/SdkConfig"; import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes"; +import { FetchError } from "@internal/manager-api/model/Errors"; import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; import { ManagerApiDataSource } from "./ManagerApiDataSource"; @@ -14,9 +16,16 @@ export class DefaultManagerApiDataSource implements ManagerApiDataSource { this.baseUrl = config.managerApiUrl; } - getAppsByHash(hashes: string[]) { - return axios - .post(`${this.baseUrl}/v2/apps/hash`, hashes) - .then((res) => res.data); + getAppsByHash( + hashes: string[], + ): EitherAsync> { + return EitherAsync(() => + axios.post>( + `${this.baseUrl}/v2/apps/hash`, + hashes, + ), + ) + .map((res) => res.data) + .mapLeft((error) => new FetchError(error)); } } diff --git a/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts index 58b9e5021..3850aae52 100644 --- a/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts +++ b/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts @@ -1,5 +1,10 @@ +import { EitherAsync } from "purify-ts"; + +import { FetchError } from "@internal/manager-api/model/Errors"; import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; export interface ManagerApiDataSource { - getAppsByHash(hashes: string[]): Promise; + getAppsByHash( + hashes: string[], + ): EitherAsync>; } diff --git a/packages/core/src/internal/manager-api/di/managerApiModule.test.ts b/packages/core/src/internal/manager-api/di/managerApiModule.test.ts index ea46b3160..4df4c70bb 100644 --- a/packages/core/src/internal/manager-api/di/managerApiModule.test.ts +++ b/packages/core/src/internal/manager-api/di/managerApiModule.test.ts @@ -2,6 +2,7 @@ import { Container } from "inversify"; import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { StubUseCase } from "@root/src/di.stub"; import { managerApiModuleFactory } from "./managerApiModule"; import { managerApiTypes } from "./managerApiTypes"; @@ -13,6 +14,7 @@ describe("managerApiModuleFactory", () => { let mod: ReturnType; beforeEach(() => { mod = managerApiModuleFactory({ + stub: false, config: { managerApiUrl: "http://fake.url" }, }); container = new Container(); @@ -23,7 +25,7 @@ describe("managerApiModuleFactory", () => { expect(mod).toBeDefined(); }); - it("should return none mocked use cases", () => { + it("should return none stubbed use cases", () => { const managerApiDataSource = container.get( managerApiTypes.ManagerApiDataSource, ); @@ -38,4 +40,36 @@ describe("managerApiModuleFactory", () => { expect(config).toEqual({ managerApiUrl: "http://fake.url" }); }); }); + + describe("Stubbed", () => { + let container: Container; + let mod: ReturnType; + beforeEach(() => { + mod = managerApiModuleFactory({ + stub: true, + config: { managerApiUrl: "http://fake.url" }, + }); + container = new Container(); + container.load(mod); + }); + + it("should return the config module", () => { + expect(mod).toBeDefined(); + }); + + it("should return stubbed use cases", () => { + const managerApiDataSource = container.get( + managerApiTypes.ManagerApiDataSource, + ); + expect(managerApiDataSource).toBeInstanceOf(StubUseCase); + + const managerApiService = container.get( + managerApiTypes.ManagerApiService, + ); + expect(managerApiService).toBeInstanceOf(StubUseCase); + + const config = container.get(managerApiTypes.SdkConfig); + expect(config).toEqual({ managerApiUrl: "http://fake.url" }); + }); + }); }); diff --git a/packages/core/src/internal/manager-api/di/managerApiModule.ts b/packages/core/src/internal/manager-api/di/managerApiModule.ts index 61e10d1da..eb954d3bf 100644 --- a/packages/core/src/internal/manager-api/di/managerApiModule.ts +++ b/packages/core/src/internal/manager-api/di/managerApiModule.ts @@ -8,14 +8,11 @@ import { StubUseCase } from "@root/src/di.stub"; import { managerApiTypes } from "./managerApiTypes"; type FactoryProps = { - stub: boolean; + stub?: boolean; config: SdkConfig; }; -export const managerApiModuleFactory = ({ - stub = false, - config, -}: Partial = {}) => +export const managerApiModuleFactory = ({ stub, config }: FactoryProps) => new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(managerApiTypes.SdkConfig).toConstantValue(config); diff --git a/packages/core/src/internal/manager-api/model/Errors.ts b/packages/core/src/internal/manager-api/model/Errors.ts new file mode 100644 index 000000000..88f3e60ec --- /dev/null +++ b/packages/core/src/internal/manager-api/model/Errors.ts @@ -0,0 +1,10 @@ +import { SdkError } from "@api/Error"; + +export class FetchError implements SdkError { + _tag = "FetchError"; + originalError?: unknown; + + constructor(public readonly error: unknown) { + this.originalError = error; + } +} diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts index 89f8ae02d..d4aeee9bc 100644 --- a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts +++ b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts @@ -1,5 +1,14 @@ +import { Left, Right } from "purify-ts"; + +import { + BTC_APP, + BTC_APP_METADATA, + ETH_APP, + ETH_APP_METADATA, +} from "@api/device-action/__test-utils__/data"; import { MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const"; import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { FetchError } from "@internal/manager-api/model/Errors"; import { DefaultManagerApiService } from "./DefaultManagerApiService"; import { ManagerApiService } from "./ManagerApiService"; @@ -15,9 +24,52 @@ describe("ManagerApiService", () => { service = new DefaultManagerApiService(dataSource); }); - it("should be defined", () => { - expect(service).toBeDefined(); - }); + describe("getAppsByHash", () => { + describe("success cases", () => { + it("with no apps, should return an empty list", async () => { + dataSource.getAppsByHash.mockResolvedValue(Right([])); + expect(await service.getAppsByHash([])).toEqual(Right([])); + }); + + it("with one app, should return the metadata", async () => { + dataSource.getAppsByHash.mockResolvedValue(Right([BTC_APP_METADATA])); + expect(await service.getAppsByHash([BTC_APP])).toEqual( + Right([BTC_APP_METADATA]), + ); + }); + + it("with two app, should return the metadata of both apps", async () => { + dataSource.getAppsByHash.mockResolvedValue( + Right([BTC_APP_METADATA, ETH_APP_METADATA]), + ); + expect(await service.getAppsByHash([BTC_APP, ETH_APP])).toEqual( + Right([BTC_APP_METADATA, ETH_APP_METADATA]), + ); + }); - // TODO: finish testing + it("with one app and one without `appFullHash`, should return the metadata of the correct app", async () => { + dataSource.getAppsByHash.mockResolvedValue(Right([BTC_APP_METADATA])); + const APP_WITH_NO_HASH = { ...ETH_APP, appFullHash: "" }; + expect( + await service.getAppsByHash([BTC_APP, APP_WITH_NO_HASH]), + ).toEqual(Right([BTC_APP_METADATA])); + }); + }); + + describe("error cases", () => { + it("should return an error when the data source fails with a known error", async () => { + const error = new FetchError(new Error("Failed to fetch data")); + dataSource.getAppsByHash.mockRejectedValue(error); + expect(await service.getAppsByHash([BTC_APP])).toEqual(Left(error)); + }); + + it("should return an error when the data source fails with an unknown error", async () => { + const error = new Error("unkown error"); + dataSource.getAppsByHash.mockRejectedValue(error); + expect(await service.getAppsByHash([BTC_APP])).toEqual( + Left(new FetchError(error)), + ); + }); + }); + }); }); diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts index 442d5125d..6f6250f6e 100644 --- a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts +++ b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts @@ -1,8 +1,10 @@ import { inject, injectable } from "inversify"; +import { EitherAsync } from "purify-ts"; import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes"; +import { FetchError } from "@internal/manager-api/model/Errors"; import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; import { ManagerApiService } from "./ManagerApiService"; @@ -16,14 +18,33 @@ export class DefaultManagerApiService implements ManagerApiService { this.dataSource = dataSource; } - getAppsByHash(_apps: ListAppsResponse): Promise { - const hashes = _apps.reduce((acc, app) => { + getAppsByHash(apps: ListAppsResponse) { + const hashes = apps.reduce((acc, app) => { if (app.appFullHash) { return acc.concat(app.appFullHash); } return acc; }, []); - return this.dataSource.getAppsByHash(hashes); + + return EitherAsync>( + async ({ fromPromise, throwE }) => { + if (hashes.length === 0) { + return []; + } + try { + const response = await fromPromise( + this.dataSource.getAppsByHash(hashes), + ); + return response; + } catch (error) { + if (error instanceof FetchError) { + return throwE(error); + } + + return throwE(new FetchError(error)); + } + }, + ); } } diff --git a/packages/core/src/internal/manager-api/service/ManagerApiService.ts b/packages/core/src/internal/manager-api/service/ManagerApiService.ts index 253dea9de..06bfa11fe 100644 --- a/packages/core/src/internal/manager-api/service/ManagerApiService.ts +++ b/packages/core/src/internal/manager-api/service/ManagerApiService.ts @@ -1,6 +1,11 @@ +import { EitherAsync } from "purify-ts"; + import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; +import { FetchError } from "@internal/manager-api/model/Errors"; import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; export interface ManagerApiService { - getAppsByHash(apps: ListAppsResponse): Promise; + getAppsByHash( + apps: ListAppsResponse, + ): EitherAsync>; } diff --git a/packages/core/src/internal/send/di/sendModule.test.ts b/packages/core/src/internal/send/di/sendModule.test.ts index 708a9ec59..f176a3b21 100644 --- a/packages/core/src/internal/send/di/sendModule.test.ts +++ b/packages/core/src/internal/send/di/sendModule.test.ts @@ -7,7 +7,7 @@ describe("sendModuleFactory", () => { let container: Container; let mod: ReturnType; beforeEach(() => { - mod = sendModuleFactory(); + mod = sendModuleFactory({ stub: false }); container = new Container(); container.load(mod); }); diff --git a/packages/core/src/internal/send/di/sendModule.ts b/packages/core/src/internal/send/di/sendModule.ts index 8bc2c7e1c..1bd7f367a 100644 --- a/packages/core/src/internal/send/di/sendModule.ts +++ b/packages/core/src/internal/send/di/sendModule.ts @@ -9,9 +9,7 @@ type FactoryProps = { stub: boolean; }; -export const sendModuleFactory = ({ - stub = false, -}: Partial = {}) => +export const sendModuleFactory = ({ stub = false }: FactoryProps) => new ContainerModule( ( bind, diff --git a/packages/core/src/internal/usb/di/usbModule.test.ts b/packages/core/src/internal/usb/di/usbModule.test.ts index bfb864a70..3764205a1 100644 --- a/packages/core/src/internal/usb/di/usbModule.test.ts +++ b/packages/core/src/internal/usb/di/usbModule.test.ts @@ -12,12 +12,12 @@ describe("usbModuleFactory", () => { let container: Container; let mod: ReturnType; beforeEach(() => { - mod = usbModuleFactory(); + mod = usbModuleFactory({ stub: false }); container = new Container(); container.load(loggerModuleFactory()); container.load( mod, - deviceModelModuleFactory(), + deviceModelModuleFactory({ stub: false }), deviceSessionModuleFactory(), ); }); diff --git a/packages/core/src/internal/usb/di/usbModule.ts b/packages/core/src/internal/usb/di/usbModule.ts index 407344293..bc3267faa 100644 --- a/packages/core/src/internal/usb/di/usbModule.ts +++ b/packages/core/src/internal/usb/di/usbModule.ts @@ -11,9 +11,7 @@ type FactoryProps = { stub: boolean; }; -export const usbModuleFactory = ({ - stub = false, -}: Partial = {}) => +export const usbModuleFactory = ({ stub = false }: FactoryProps) => new ContainerModule((bind, _unbind, _isBound, rebind) => { // The transport needs to be a singleton to keep the internal states of the devices bind(usbDiTypes.UsbHidTransport).to(WebUsbHidTransport).inSingletonScope(); From 71e62dcb87e4684461cb6d737d7cd8427e5259a3 Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Wed, 31 Jul 2024 17:33:11 +0200 Subject: [PATCH 25/46] =?UTF-8?q?=F0=9F=8E=A8=20(core):=20Update=20device?= =?UTF-8?q?=20session=20to=20pass=20getAppsByHashes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/api/DeviceSdkBuilder.ts | 4 +- .../use-case/SendCommandUseCase.test.ts | 4 +- .../src/api/device-action/DeviceAction.ts | 2 +- .../api/device-action/__test-utils__/data.ts | 4 +- .../__test-utils__/makeInternalApi.ts | 6 +- .../GetDeviceStatusDeviceAction.test.ts | 22 +++---- .../GoToDashboardDeviceAction.test.ts | 22 +++---- .../os/ListApps/ListAppsDeviceAction.test.ts | 24 +++---- .../ListAppsWithMetadataDeviceAction.test.ts | 58 +++++++--------- .../ListAppsWithMetadataDeviceAction.ts | 11 ++-- .../os/ListAppsWithMetadata/types.ts | 4 +- .../OpenAppDeviceAction.test.ts | 26 ++++---- .../SendCommandInAppDeviceAction.test.ts | 22 ++++--- .../api/device-session/DeviceSessionState.ts | 4 +- packages/core/src/di.ts | 4 +- .../device-session/model/DeviceSession.ts | 4 +- .../DefaultDeviceSessionService.test.ts | 6 +- .../GetDeviceSessionStateUseCase.test.ts | 6 +- .../discovery/use-case/ConnectUseCase.test.ts | 6 +- .../use-case/DisconnectUseCase.test.ts | 4 +- ...t.ts => AxiosManagerApiDataSource.test.ts} | 26 ++++---- .../data/AxiosManagerApiDataSource.ts | 66 +++++++++++++++++++ .../data/DefaultManagerApiDataSource.ts | 31 --------- .../manager-api/data/ManagerApiDataSource.ts | 6 +- .../manager-api/data/ManagerApiDto.ts | 34 ++++++++++ ...Source.ts => AxiosManagerApiDataSource.ts} | 2 +- .../manager-api/di/managerApiModule.test.ts | 4 +- .../manager-api/di/managerApiModule.ts | 4 +- .../src/internal/manager-api/model/Const.ts | 3 +- .../src/internal/manager-api/model/Errors.ts | 2 +- ...nagerApiResponses.ts => ManagerApiType.ts} | 2 +- .../service/DefaultManagerApiService.test.ts | 20 +++--- .../service/DefaultManagerApiService.ts | 10 +-- .../manager-api/service/ManagerApiService.ts | 6 +- .../send/use-case/SendApduUseCase.test.ts | 6 +- .../GetConnectedDeviceUseCase.test.ts | 6 +- 36 files changed, 269 insertions(+), 202 deletions(-) rename packages/core/src/internal/manager-api/data/{DefaultManagerApiDataSource.test.ts => AxiosManagerApiDataSource.test.ts} (71%) create mode 100644 packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.ts delete mode 100644 packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts create mode 100644 packages/core/src/internal/manager-api/data/ManagerApiDto.ts rename packages/core/src/internal/manager-api/data/__mocks__/{DefaultManagerApiDataSource.ts => AxiosManagerApiDataSource.ts} (61%) rename packages/core/src/internal/manager-api/model/{ManagerApiResponses.ts => ManagerApiType.ts} (94%) diff --git a/packages/core/src/api/DeviceSdkBuilder.ts b/packages/core/src/api/DeviceSdkBuilder.ts index b2589ca11..12b38e942 100644 --- a/packages/core/src/api/DeviceSdkBuilder.ts +++ b/packages/core/src/api/DeviceSdkBuilder.ts @@ -1,4 +1,4 @@ -import { MANAGER_API_BASE_URL } from "@internal/manager-api/model/Const"; +import { DEFAULT_MANAGER_API_BASE_URL } from "@internal/manager-api/model/Const"; import { LoggerSubscriberService } from "./logger-subscriber/service/LoggerSubscriberService"; import { DeviceSdk } from "./DeviceSdk"; @@ -19,7 +19,7 @@ export class LedgerDeviceSdkBuilder { private stub = false; private readonly loggers: LoggerSubscriberService[] = []; private config: SdkConfig = { - managerApiUrl: MANAGER_API_BASE_URL, + managerApiUrl: DEFAULT_MANAGER_API_BASE_URL, }; build(): DeviceSdk { diff --git a/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts b/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts index 13ac2c7cb..412df80cf 100644 --- a/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts +++ b/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts @@ -6,7 +6,7 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; @@ -24,7 +24,7 @@ describe("SendCommandUseCase", () => { beforeEach(() => { logger = new DefaultLoggerPublisherService([], "send-command-use-case"); sessionService = new DefaultDeviceSessionService(() => logger); - managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiDataSource = new AxiosManagerApiDataSource({ managerApiUrl: "http://fake.url", }); managerApi = new DefaultManagerApiService(managerApiDataSource); diff --git a/packages/core/src/api/device-action/DeviceAction.ts b/packages/core/src/api/device-action/DeviceAction.ts index 4b6c9e438..ba404f1bd 100644 --- a/packages/core/src/api/device-action/DeviceAction.ts +++ b/packages/core/src/api/device-action/DeviceAction.ts @@ -16,7 +16,7 @@ export type InternalApi = { readonly setDeviceSessionState: ( state: DeviceSessionState, ) => DeviceSessionState; - managerApiService: ManagerApiService; + getMetadataForAppHashes: ManagerApiService["getAppsByHash"]; }; export type DeviceActionIntermediateValue = { diff --git a/packages/core/src/api/device-action/__test-utils__/data.ts b/packages/core/src/api/device-action/__test-utils__/data.ts index 1b5e24e05..2057d573a 100644 --- a/packages/core/src/api/device-action/__test-utils__/data.ts +++ b/packages/core/src/api/device-action/__test-utils__/data.ts @@ -1,4 +1,4 @@ -import { AppType } from "@internal/manager-api/model/ManagerApiResponses"; +import { AppType } from "@internal/manager-api/model/ManagerApiType"; export const BTC_APP = { appEntryLength: 77, @@ -9,6 +9,7 @@ export const BTC_APP = { "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb", appName: "Bitcoin", }; + export const BTC_APP_METADATA = { versionId: 36248, versionName: "Bitcoin", @@ -39,6 +40,7 @@ export const BTC_APP_METADATA = { parent: null, parentName: null, }; + export const CUSTOM_LOCK_SCREEN_APP = { appEntryLength: 70, appSizeInBlocks: 1093, diff --git a/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts b/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts index 880ae7a64..5db4c7416 100644 --- a/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts +++ b/packages/core/src/api/device-action/__test-utils__/makeInternalApi.ts @@ -4,14 +4,14 @@ const sendCommandMock = jest.fn(); const apiGetDeviceSessionStateMock = jest.fn(); const apiGetDeviceSessionStateObservableMock = jest.fn(); const setDeviceSessionStateMock = jest.fn(); -const managerApiServiceMock = { getAppsByHash: jest.fn() }; +const getMetadataForAppHashesMock = jest.fn(); -export function makeInternalApiMock(): jest.Mocked { +export function makeDeviceActionInternalApiMock(): jest.Mocked { return { sendCommand: sendCommandMock, getDeviceSessionState: apiGetDeviceSessionStateMock, getDeviceSessionStateObservable: apiGetDeviceSessionStateObservableMock, setDeviceSessionState: setDeviceSessionStateMock, - managerApiService: managerApiServiceMock, + getMetadataForAppHashes: getMetadataForAppHashesMock, }; } diff --git a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts index 3c01af633..d738575d9 100644 --- a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts @@ -1,7 +1,7 @@ import { interval, Observable } from "rxjs"; import { DeviceStatus } from "@api/device/DeviceStatus"; -import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { makeDeviceActionInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; @@ -36,7 +36,7 @@ describe("GetDeviceStatusDeviceAction", () => { sendCommand: sendCommandMock, getDeviceSessionState: apiGetDeviceSessionStateMock, getDeviceSessionStateObservable: apiGetDeviceSessionStateObservableMock, - } = makeInternalApiMock(); + } = makeDeviceActionInternalApiMock(); beforeEach(() => { jest.resetAllMocks(); isDeviceOnboardedMock.mockReturnValue(true); @@ -83,7 +83,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -168,7 +168,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -220,7 +220,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -306,7 +306,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -339,7 +339,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -396,7 +396,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -472,7 +472,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -555,7 +555,7 @@ describe("GetDeviceStatusDeviceAction", () => { testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -593,7 +593,7 @@ describe("GetDeviceStatusDeviceAction", () => { const { cancel } = testDeviceActionStates( getDeviceStateDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); cancel(); diff --git a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts b/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts index 8d08a3e46..225461ef6 100644 --- a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts @@ -1,5 +1,5 @@ import { DeviceStatus } from "@api/device/DeviceStatus"; -import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { makeDeviceActionInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { setupGetDeviceStatusMock } from "@api/device-action/__test-utils__/setupTestMachine"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; @@ -30,7 +30,7 @@ describe("GoToDashboardDeviceAction", () => { const { sendCommand: sendCommandMock, getDeviceSessionState: apiGetDeviceSessionStateMock, - } = makeInternalApiMock(); + } = makeDeviceActionInternalApiMock(); beforeEach(() => { jest.resetAllMocks(); @@ -79,7 +79,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -146,7 +146,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -203,7 +203,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -258,7 +258,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -301,7 +301,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -357,7 +357,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -421,7 +421,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -486,7 +486,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -561,7 +561,7 @@ describe("GoToDashboardDeviceAction", () => { testDeviceActionStates( goToDashboardDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); diff --git a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts b/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts index efa48f675..13b2ccd8f 100644 --- a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts @@ -6,7 +6,7 @@ import { ETH_APP, SOLANA_APP, } from "@api/device-action/__test-utils__/data"; -import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { makeDeviceActionInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { setupGoToDashboardMock } from "@api/device-action/__test-utils__/setupTestMachine"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; @@ -22,7 +22,7 @@ import { ListAppsDAState } from "./types"; jest.mock("@api/device-action/os/GoToDashboard/GoToDashboardDeviceAction"); describe("ListAppsDeviceAction", () => { - const { sendCommand: sendCommandMock } = makeInternalApiMock(); + const { sendCommand: sendCommandMock } = makeDeviceActionInternalApiMock(); beforeEach(() => { jest.resetAllMocks(); @@ -65,7 +65,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -106,7 +106,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -155,7 +155,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -204,7 +204,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -260,7 +260,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -321,7 +321,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -358,7 +358,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -393,7 +393,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -434,7 +434,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -483,7 +483,7 @@ describe("ListAppsDeviceAction", () => { testDeviceActionStates( listAppsDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts index d21c45f16..eb5375ffc 100644 --- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts @@ -9,14 +9,14 @@ import { ETH_APP, ETH_APP_METADATA, } from "@api/device-action/__test-utils__/data"; -import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { makeDeviceActionInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { setupListAppsMock } from "@api/device-action/__test-utils__/setupTestMachine"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; import { UnknownDAError } from "@api/device-action/os/Errors"; import { DeviceSessionStateType } from "@api/device-session/DeviceSessionState"; -import { FetchError } from "@internal/manager-api/model/Errors"; +import { HttpFetchApiError } from "@internal/manager-api/model/Errors"; import { ListAppsWithMetadataDeviceAction } from "./ListAppsWithMetadataDeviceAction"; import { ListAppsWithMetadataDAState } from "./types"; @@ -25,10 +25,10 @@ jest.mock("@api/device-action/os/ListApps/ListAppsDeviceAction"); describe("ListAppsWithMetadataDeviceAction", () => { const { - managerApiService: managerApiServiceMock, + getMetadataForAppHashes: getMetadataForAppHashesMock, // getDeviceSessionState: apiGetDeviceSessionStateMock, // setDeviceSessionState: apiSetDeviceSessionStateMock, - } = makeInternalApiMock(); + } = makeDeviceActionInternalApiMock(); const saveSessionStateMock = jest.fn(); const getDeviceSessionStateMock = jest.fn(); @@ -54,9 +54,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { input: {}, }); - jest - .spyOn(managerApiServiceMock, "getAppsByHash") - .mockResolvedValue(Right([])); + getMetadataForAppHashesMock.mockResolvedValue(Right([])); const expectedStates: Array = [ { @@ -80,7 +78,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { testDeviceActionStates( listAppsWithMetadataDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -92,9 +90,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { input: {}, }); - jest - .spyOn(managerApiServiceMock, "getAppsByHash") - .mockResolvedValue(Right([BTC_APP_METADATA])); + getMetadataForAppHashesMock.mockResolvedValue(Right([BTC_APP_METADATA])); const expectedStates: Array = [ { @@ -130,7 +126,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { testDeviceActionStates( listAppsWithMetadataDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -142,9 +138,9 @@ describe("ListAppsWithMetadataDeviceAction", () => { input: {}, }); - jest - .spyOn(managerApiServiceMock, "getAppsByHash") - .mockResolvedValue(Right([BTC_APP_METADATA, ETH_APP_METADATA])); + getMetadataForAppHashesMock.mockResolvedValue( + Right([BTC_APP_METADATA, ETH_APP_METADATA]), + ); const expectedStates: Array = [ { @@ -180,7 +176,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { testDeviceActionStates( listAppsWithMetadataDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -192,11 +188,9 @@ describe("ListAppsWithMetadataDeviceAction", () => { input: {}, }); - jest - .spyOn(managerApiServiceMock, "getAppsByHash") - .mockResolvedValue( - Right([BTC_APP_METADATA, CUSTOM_LOCK_SCREEN_APP_METADATA]), - ); + getMetadataForAppHashesMock.mockResolvedValue( + Right([BTC_APP_METADATA, CUSTOM_LOCK_SCREEN_APP_METADATA]), + ); const expectedStates: Array = [ { @@ -232,7 +226,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { testDeviceActionStates( listAppsWithMetadataDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -268,7 +262,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { testDeviceActionStates( listAppsWithMetadataDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -280,9 +274,9 @@ describe("ListAppsWithMetadataDeviceAction", () => { input: {}, }); - jest - .spyOn(managerApiServiceMock, "getAppsByHash") - .mockRejectedValue(new UnknownDAError("getAppsByHash failed")); + getMetadataForAppHashesMock.mockRejectedValue( + new UnknownDAError("getAppsByHash failed"), + ); const expectedStates: Array = [ { @@ -312,7 +306,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { testDeviceActionStates( listAppsWithMetadataDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -324,11 +318,9 @@ describe("ListAppsWithMetadataDeviceAction", () => { input: {}, }); - const error = new FetchError(new Error("Failed to fetch data")); + const error = new HttpFetchApiError(new Error("Failed to fetch data")); - jest - .spyOn(managerApiServiceMock, "getAppsByHash") - .mockResolvedValue(Left(error)); + getMetadataForAppHashesMock.mockResolvedValue(Left(error)); const expectedStates: Array = [ { @@ -358,7 +350,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { testDeviceActionStates( listAppsWithMetadataDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -423,7 +415,7 @@ describe("ListAppsWithMetadataDeviceAction", () => { testDeviceActionStates( listAppsWithMetadataDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts index 909a2e6d5..2864bc4ab 100644 --- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts @@ -16,8 +16,8 @@ import { ListAppsDAOutput } from "@api/device-action/os/ListApps/types"; import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes"; import { XStateDeviceAction } from "@api/device-action/xstate-utils/XStateDeviceAction"; import { DeviceSessionState } from "@api/device-session/DeviceSessionState"; -import { FetchError } from "@internal/manager-api/model/Errors"; -import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; +import { HttpFetchApiError } from "@internal/manager-api/model/Errors"; +import { Application } from "@internal/manager-api/model/ManagerApiType"; import { ListAppsWithMetadataDAError, @@ -37,7 +37,7 @@ export type MachineDependencies = { input, }: { input: ListAppsDAOutput; - }) => EitherAsync>; + }) => EitherAsync>; getDeviceSessionState: () => DeviceSessionState; saveSessionState: (state: DeviceSessionState) => DeviceSessionState; }; @@ -87,7 +87,7 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< }: { sendBack: (event: AnyEventObject) => void; input: { - appsWithMetadata: Array; + appsWithMetadata: Array; }; }) => { const { appsWithMetadata } = input; @@ -283,8 +283,7 @@ export class ListAppsWithMetadataDeviceAction extends XStateDeviceAction< extractDependencies(internalApi: InternalApi): MachineDependencies { return { - getAppsByHash: ({ input }) => - internalApi.managerApiService.getAppsByHash(input), + getAppsByHash: ({ input }) => internalApi.getMetadataForAppHashes(input), getDeviceSessionState: () => internalApi.getDeviceSessionState(), saveSessionState: (state: DeviceSessionState) => internalApi.setDeviceSessionState(state), diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts b/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts index 23a9505dd..4181677ac 100644 --- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts +++ b/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts @@ -7,9 +7,9 @@ import { ListAppsDAIntermediateValue, } from "@api/device-action/os/ListApps/types"; import { SdkError } from "@api/Error"; -import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; +import { Application } from "@internal/manager-api/model/ManagerApiType"; -export type ListAppsWithMetadataDAOutput = Array; +export type ListAppsWithMetadataDAOutput = Array; export type ListAppsWithMetadataDAInput = ListAppsDAInput; export type ListAppsWithMetadataDAError = diff --git a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts b/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts index 36dc2f852..d939f1c3f 100644 --- a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts @@ -1,6 +1,6 @@ import { InvalidStatusWordError } from "@api/command/Errors"; import { DeviceStatus } from "@api/device/DeviceStatus"; -import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { makeDeviceActionInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; @@ -33,7 +33,7 @@ describe("OpenAppDeviceAction", () => { const { sendCommand: sendCommandMock, getDeviceSessionState: apiGetDeviceSessionStateMock, - } = makeInternalApiMock(); + } = makeDeviceActionInternalApiMock(); beforeEach(() => { jest.resetAllMocks(); @@ -74,7 +74,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -115,7 +115,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -161,7 +161,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -213,7 +213,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -246,7 +246,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -276,7 +276,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -316,7 +316,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -361,7 +361,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -409,7 +409,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -462,7 +462,7 @@ describe("OpenAppDeviceAction", () => { testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -501,7 +501,7 @@ describe("OpenAppDeviceAction", () => { const { cancel } = testDeviceActionStates( openAppDeviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); cancel(); diff --git a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts b/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts index b52e05e28..f9992931f 100644 --- a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts +++ b/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts @@ -3,7 +3,7 @@ import { assign, createMachine } from "xstate"; import { Apdu } from "@api/apdu/model/Apdu"; import { ApduBuilder } from "@api/apdu/utils/ApduBuilder"; -import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; +import { makeDeviceActionInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi"; import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates"; import { DeviceActionState, @@ -67,7 +67,7 @@ describe("SendCommandInAppDeviceAction", () => { sendCommand: sendMyCommand, }); - const { sendCommand: apiSendCommandMock } = makeInternalApiMock(); + const { sendCommand: apiSendCommandMock } = makeDeviceActionInternalApiMock(); const commandParams = { paramString: "aParameter", @@ -94,11 +94,13 @@ describe("SendCommandInAppDeviceAction", () => { }, }); await new Promise((resolve, reject) => { - deviceAction._execute(makeInternalApiMock()).observable.subscribe({ - error: () => reject(), - complete: () => resolve(), - next: () => {}, - }); + deviceAction + ._execute(makeDeviceActionInternalApiMock()) + .observable.subscribe({ + error: () => reject(), + complete: () => resolve(), + next: () => {}, + }); }); expect(apiSendCommandMock).toHaveBeenCalledWith( @@ -139,7 +141,7 @@ describe("SendCommandInAppDeviceAction", () => { }, }), expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -189,7 +191,7 @@ describe("SendCommandInAppDeviceAction", () => { testDeviceActionStates( deviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); @@ -241,7 +243,7 @@ describe("SendCommandInAppDeviceAction", () => { testDeviceActionStates( deviceAction, expectedStates, - makeInternalApiMock(), + makeDeviceActionInternalApiMock(), done, ); }); diff --git a/packages/core/src/api/device-session/DeviceSessionState.ts b/packages/core/src/api/device-session/DeviceSessionState.ts index 7bf3ec628..d4843b62e 100644 --- a/packages/core/src/api/device-session/DeviceSessionState.ts +++ b/packages/core/src/api/device-session/DeviceSessionState.ts @@ -1,6 +1,6 @@ import { BatteryStatusFlags } from "@api/command/os/GetBatteryStatusCommand"; import { DeviceStatus } from "@api/device/DeviceStatus"; -import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; +import { Application } from "@internal/manager-api/model/ManagerApiType"; /** * The battery status of a device. @@ -77,7 +77,7 @@ type DeviceSessionReadyState = { /** * The current applications installed on the device. */ - readonly installedApps: ApplicationEntity[]; + readonly installedApps: Application[]; }; /** diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index b59bfabad..25e5595c5 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -12,7 +12,7 @@ import { deviceSessionModuleFactory } from "@internal/device-session/di/deviceSe import { discoveryModuleFactory } from "@internal/discovery/di/discoveryModule"; import { loggerModuleFactory } from "@internal/logger-publisher/di/loggerModule"; import { managerApiModuleFactory } from "@internal/manager-api/di/managerApiModule"; -import { MANAGER_API_BASE_URL } from "@internal/manager-api/model/Const"; +import { DEFAULT_MANAGER_API_BASE_URL } from "@internal/manager-api/model/Const"; import { sendModuleFactory } from "@internal/send/di/sendModule"; import { usbModuleFactory } from "@internal/usb/di/usbModule"; @@ -28,7 +28,7 @@ export type MakeContainerProps = { export const makeContainer = ({ stub = false, loggers = [], - config = { managerApiUrl: MANAGER_API_BASE_URL }, + config = { managerApiUrl: DEFAULT_MANAGER_API_BASE_URL }, }: MakeContainerProps) => { const container = new Container(); diff --git a/packages/core/src/internal/device-session/model/DeviceSession.ts b/packages/core/src/internal/device-session/model/DeviceSession.ts index 84e066a5d..1b83f36e0 100644 --- a/packages/core/src/internal/device-session/model/DeviceSession.ts +++ b/packages/core/src/internal/device-session/model/DeviceSession.ts @@ -2,6 +2,7 @@ import { BehaviorSubject } from "rxjs"; import { v4 as uuidv4 } from "uuid"; import { Command } from "@api/command/Command"; +import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; import { CommandUtils } from "@api/command/utils/CommandUtils"; import { DeviceStatus } from "@api/device/DeviceStatus"; import { @@ -149,7 +150,8 @@ export class DeviceSession { this.setDeviceSessionState(state); return this._deviceState.getValue(); }, - managerApiService: this._managerApiService, + getMetadataForAppHashes: (apps: ListAppsResponse) => + this._managerApiService.getAppsByHash(apps), }); return { diff --git a/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts b/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts index 91734c37b..7f382eef0 100644 --- a/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts +++ b/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts @@ -3,7 +3,7 @@ import { Either, Left } from "purify-ts"; import { DeviceSession } from "@internal/device-session/model/DeviceSession"; import { DeviceSessionNotFound } from "@internal/device-session/model/Errors"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import type { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; @@ -12,7 +12,7 @@ import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnecte import { DefaultDeviceSessionService } from "./DefaultDeviceSessionService"; jest.mock("@internal/logger-publisher/service/DefaultLoggerPublisherService"); -jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); +jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource"); let sessionService: DefaultDeviceSessionService; let loggerService: DefaultLoggerPublisherService; @@ -24,7 +24,7 @@ describe("DefaultDeviceSessionService", () => { jest.restoreAllMocks(); loggerService = new DefaultLoggerPublisherService([], "deviceSession"); sessionService = new DefaultDeviceSessionService(() => loggerService); - managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiDataSource = new AxiosManagerApiDataSource({ managerApiUrl: "http://fake.url", }); managerApi = new DefaultManagerApiService(managerApiDataSource); diff --git a/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts b/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts index ab07c305c..d00043301 100644 --- a/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts +++ b/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts @@ -3,14 +3,14 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { GetDeviceSessionStateUseCase } from "./GetDeviceSessionStateUseCase"; -jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); +jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource"); let logger: LoggerPublisherService; let sessionService: DeviceSessionService; @@ -25,7 +25,7 @@ describe("GetDeviceSessionStateUseCase", () => { [], "get-connected-device-use-case-test", ); - managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiDataSource = new AxiosManagerApiDataSource({ managerApiUrl: "http://fake.url", }); managerApi = new DefaultManagerApiService(managerApiDataSource); diff --git a/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts b/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts index 937240599..62cf8394c 100644 --- a/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts +++ b/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts @@ -7,7 +7,7 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; @@ -18,7 +18,7 @@ import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport"; import { ConnectUseCase } from "./ConnectUseCase"; -jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); +jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource"); let transport: WebUsbHidTransport; let logger: LoggerPublisherService; @@ -40,7 +40,7 @@ describe("ConnectUseCase", () => { usbHidDeviceConnectionFactoryStubBuilder(), ); sessionService = new DefaultDeviceSessionService(() => logger); - managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiDataSource = new AxiosManagerApiDataSource({ managerApiUrl: "http://fake.url", }); managerApi = new DefaultManagerApiService(managerApiDataSource); diff --git a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts b/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts index d516719f9..49b18d3ce 100644 --- a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts +++ b/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts @@ -5,7 +5,7 @@ import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceS import { DeviceSessionNotFound } from "@internal/device-session/model/Errors"; import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; @@ -42,7 +42,7 @@ describe("DisconnectUseCase", () => { it("should disconnect from a device", async () => { // Given const connectedDevice = connectedDeviceStubBuilder(); - managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiDataSource = new AxiosManagerApiDataSource({ managerApiUrl: "http://fake.url", }); managerApi = new DefaultManagerApiService(managerApiDataSource); diff --git a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts b/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts similarity index 71% rename from packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts rename to packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts index 58bbd32a0..f9368d439 100644 --- a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.test.ts +++ b/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts @@ -7,19 +7,19 @@ import { CUSTOM_LOCK_SCREEN_APP, CUSTOM_LOCK_SCREEN_APP_METADATA, } from "@api/device-action/__test-utils__/data"; -import { MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const"; -import { FetchError } from "@internal/manager-api/model/Errors"; +import { DEFAULT_MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const"; +import { HttpFetchApiError } from "@internal/manager-api/model/Errors"; -import { DefaultManagerApiDataSource } from "./DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "./AxiosManagerApiDataSource"; jest.mock("axios"); -describe("DefaultManagerApiDataSource", () => { +describe("AxiosManagerApiDataSource", () => { describe("getAppsByHash", () => { describe("success cases", () => { it("with BTC app, should return the metadata", async () => { - const api = new DefaultManagerApiDataSource({ - managerApiUrl: MANAGER_API_BASE_URL, + const api = new AxiosManagerApiDataSource({ + managerApiUrl: DEFAULT_MANAGER_API_BASE_URL, }); jest.spyOn(axios, "post").mockResolvedValue({ @@ -34,8 +34,8 @@ describe("DefaultManagerApiDataSource", () => { }); it("with no apps, should return an empty list", async () => { - const api = new DefaultManagerApiDataSource({ - managerApiUrl: MANAGER_API_BASE_URL, + const api = new AxiosManagerApiDataSource({ + managerApiUrl: DEFAULT_MANAGER_API_BASE_URL, }); jest.spyOn(axios, "post").mockResolvedValue({ @@ -50,8 +50,8 @@ describe("DefaultManagerApiDataSource", () => { }); it("with BTC app and custom lock screen, should return the metadata", async () => { - const api = new DefaultManagerApiDataSource({ - managerApiUrl: MANAGER_API_BASE_URL, + const api = new AxiosManagerApiDataSource({ + managerApiUrl: DEFAULT_MANAGER_API_BASE_URL, }); jest.spyOn(axios, "post").mockResolvedValue({ @@ -73,8 +73,8 @@ describe("DefaultManagerApiDataSource", () => { describe("error cases", () => { it("should throw an error if the request fails", async () => { - const api = new DefaultManagerApiDataSource({ - managerApiUrl: MANAGER_API_BASE_URL, + const api = new AxiosManagerApiDataSource({ + managerApiUrl: DEFAULT_MANAGER_API_BASE_URL, }); const err = new Error("fetch error"); jest.spyOn(axios, "post").mockRejectedValue(err); @@ -84,7 +84,7 @@ describe("DefaultManagerApiDataSource", () => { try { await api.getAppsByHash(hashes); } catch (error) { - expect(error).toEqual(Left(new FetchError(err))); + expect(error).toEqual(Left(new HttpFetchApiError(err))); } }); }); diff --git a/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.ts new file mode 100644 index 000000000..c586991a3 --- /dev/null +++ b/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.ts @@ -0,0 +1,66 @@ +import axios from "axios"; +import { inject, injectable } from "inversify"; +import { EitherAsync } from "purify-ts"; + +import { type SdkConfig } from "@api/SdkConfig"; +import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes"; +import { HttpFetchApiError } from "@internal/manager-api/model/Errors"; +import { + Application, + AppType, +} from "@internal/manager-api/model/ManagerApiType"; + +import { ManagerApiDataSource } from "./ManagerApiDataSource"; +import { ApplicationDto, AppTypeDto } from "./ManagerApiDto"; + +@injectable() +export class AxiosManagerApiDataSource implements ManagerApiDataSource { + private readonly baseUrl: string; + constructor(@inject(managerApiTypes.SdkConfig) config: SdkConfig) { + this.baseUrl = config.managerApiUrl; + } + + private mapAppTypeDtoToAppType(appType: AppTypeDto): AppType { + switch (appType) { + case AppTypeDto.currency: + return AppType.currency; + case AppTypeDto.plugin: + return AppType.plugin; + case AppTypeDto.tool: + return AppType.tool; + case AppTypeDto.swap: + return AppType.swap; + } + } + + private mapApplicationDtoToApplication( + apps: Array, + ): Array { + return apps.map((app) => { + if (app === null) { + return null; + } + + const { applicationType, ...rest } = app; + + return { + ...rest, + applicationType: this.mapAppTypeDtoToAppType(applicationType), + }; + }); + } + + getAppsByHash( + hashes: string[], + ): EitherAsync> { + return EitherAsync(() => + axios.post>( + `${this.baseUrl}/v2/apps/hash`, + hashes, + ), + ) + .map((res) => res.data) + .map((apps) => this.mapApplicationDtoToApplication(apps)) + .mapLeft((error) => new HttpFetchApiError(error)); + } +} diff --git a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts deleted file mode 100644 index b4e7f521f..000000000 --- a/packages/core/src/internal/manager-api/data/DefaultManagerApiDataSource.ts +++ /dev/null @@ -1,31 +0,0 @@ -import axios from "axios"; -import { inject, injectable } from "inversify"; -import { EitherAsync } from "purify-ts"; - -import { type SdkConfig } from "@api/SdkConfig"; -import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes"; -import { FetchError } from "@internal/manager-api/model/Errors"; -import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; - -import { ManagerApiDataSource } from "./ManagerApiDataSource"; - -@injectable() -export class DefaultManagerApiDataSource implements ManagerApiDataSource { - private readonly baseUrl: string; - constructor(@inject(managerApiTypes.SdkConfig) config: SdkConfig) { - this.baseUrl = config.managerApiUrl; - } - - getAppsByHash( - hashes: string[], - ): EitherAsync> { - return EitherAsync(() => - axios.post>( - `${this.baseUrl}/v2/apps/hash`, - hashes, - ), - ) - .map((res) => res.data) - .mapLeft((error) => new FetchError(error)); - } -} diff --git a/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts index 3850aae52..e5a90491e 100644 --- a/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts +++ b/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts @@ -1,10 +1,10 @@ import { EitherAsync } from "purify-ts"; -import { FetchError } from "@internal/manager-api/model/Errors"; -import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; +import { HttpFetchApiError } from "@internal/manager-api/model/Errors"; +import { Application } from "@internal/manager-api/model/ManagerApiType"; export interface ManagerApiDataSource { getAppsByHash( hashes: string[], - ): EitherAsync>; + ): EitherAsync>; } diff --git a/packages/core/src/internal/manager-api/data/ManagerApiDto.ts b/packages/core/src/internal/manager-api/data/ManagerApiDto.ts new file mode 100644 index 000000000..7b1f06a55 --- /dev/null +++ b/packages/core/src/internal/manager-api/data/ManagerApiDto.ts @@ -0,0 +1,34 @@ +export type Id = number; + +export enum AppTypeDto { + currency = "currency", + plugin = "plugin", + tool = "tool", + swap = "swap", +} + +export type ApplicationDto = { + versionId: Id; + versionName: string; + versionDisplayName: string; + version: string; + currencyId: string; + description: string; + applicationType: AppTypeDto; + dateModified: string; + icon: string; + authorName: string; + supportURL: string; + contactURL: string; + sourceURL: string; + hash: string; + perso: string; + parentName: string | null; + firmware: string; + firmwareKey: string; + delete: string; + deleteKey: string; + bytes: number; + warning: string | null; + isDevTools: boolean; +}; diff --git a/packages/core/src/internal/manager-api/data/__mocks__/DefaultManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts similarity index 61% rename from packages/core/src/internal/manager-api/data/__mocks__/DefaultManagerApiDataSource.ts rename to packages/core/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts index 2a56ef6b6..1f3c0076a 100644 --- a/packages/core/src/internal/manager-api/data/__mocks__/DefaultManagerApiDataSource.ts +++ b/packages/core/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts @@ -1,5 +1,5 @@ import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; -export class DefaultManagerApiDataSource implements ManagerApiDataSource { +export class AxiosManagerApiDataSource implements ManagerApiDataSource { getAppsByHash = jest.fn(); } diff --git a/packages/core/src/internal/manager-api/di/managerApiModule.test.ts b/packages/core/src/internal/manager-api/di/managerApiModule.test.ts index 4df4c70bb..db5e73882 100644 --- a/packages/core/src/internal/manager-api/di/managerApiModule.test.ts +++ b/packages/core/src/internal/manager-api/di/managerApiModule.test.ts @@ -1,6 +1,6 @@ import { Container } from "inversify"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import { StubUseCase } from "@root/src/di.stub"; @@ -29,7 +29,7 @@ describe("managerApiModuleFactory", () => { const managerApiDataSource = container.get( managerApiTypes.ManagerApiDataSource, ); - expect(managerApiDataSource).toBeInstanceOf(DefaultManagerApiDataSource); + expect(managerApiDataSource).toBeInstanceOf(AxiosManagerApiDataSource); const managerApiService = container.get( managerApiTypes.ManagerApiService, diff --git a/packages/core/src/internal/manager-api/di/managerApiModule.ts b/packages/core/src/internal/manager-api/di/managerApiModule.ts index eb954d3bf..f0fed0209 100644 --- a/packages/core/src/internal/manager-api/di/managerApiModule.ts +++ b/packages/core/src/internal/manager-api/di/managerApiModule.ts @@ -1,7 +1,7 @@ import { ContainerModule } from "inversify"; import { SdkConfig } from "@api/SdkConfig"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import { StubUseCase } from "@root/src/di.stub"; @@ -16,7 +16,7 @@ export const managerApiModuleFactory = ({ stub, config }: FactoryProps) => new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(managerApiTypes.SdkConfig).toConstantValue(config); - bind(managerApiTypes.ManagerApiDataSource).to(DefaultManagerApiDataSource); + bind(managerApiTypes.ManagerApiDataSource).to(AxiosManagerApiDataSource); bind(managerApiTypes.ManagerApiService).to(DefaultManagerApiService); if (stub) { diff --git a/packages/core/src/internal/manager-api/model/Const.ts b/packages/core/src/internal/manager-api/model/Const.ts index 8b8dbbb1f..6097ce49a 100644 --- a/packages/core/src/internal/manager-api/model/Const.ts +++ b/packages/core/src/internal/manager-api/model/Const.ts @@ -1 +1,2 @@ -export const MANAGER_API_BASE_URL = "https://manager.api.live.ledger.com/api"; +export const DEFAULT_MANAGER_API_BASE_URL = + "https://manager.api.live.ledger.com/api"; diff --git a/packages/core/src/internal/manager-api/model/Errors.ts b/packages/core/src/internal/manager-api/model/Errors.ts index 88f3e60ec..7d8f117be 100644 --- a/packages/core/src/internal/manager-api/model/Errors.ts +++ b/packages/core/src/internal/manager-api/model/Errors.ts @@ -1,6 +1,6 @@ import { SdkError } from "@api/Error"; -export class FetchError implements SdkError { +export class HttpFetchApiError implements SdkError { _tag = "FetchError"; originalError?: unknown; diff --git a/packages/core/src/internal/manager-api/model/ManagerApiResponses.ts b/packages/core/src/internal/manager-api/model/ManagerApiType.ts similarity index 94% rename from packages/core/src/internal/manager-api/model/ManagerApiResponses.ts rename to packages/core/src/internal/manager-api/model/ManagerApiType.ts index 40ee6b0b2..a7bec5818 100644 --- a/packages/core/src/internal/manager-api/model/ManagerApiResponses.ts +++ b/packages/core/src/internal/manager-api/model/ManagerApiType.ts @@ -7,7 +7,7 @@ export enum AppType { swap = "swap", } -export type ApplicationEntity = { +export type Application = { versionId: Id; versionName: string; versionDisplayName: string; diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts index d4aeee9bc..d2c2f45c5 100644 --- a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts +++ b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts @@ -6,21 +6,21 @@ import { ETH_APP, ETH_APP_METADATA, } from "@api/device-action/__test-utils__/data"; -import { MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; -import { FetchError } from "@internal/manager-api/model/Errors"; +import { DEFAULT_MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; +import { HttpFetchApiError } from "@internal/manager-api/model/Errors"; import { DefaultManagerApiService } from "./DefaultManagerApiService"; import { ManagerApiService } from "./ManagerApiService"; -jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); -let dataSource: jest.Mocked; +jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource"); +let dataSource: jest.Mocked; let service: ManagerApiService; describe("ManagerApiService", () => { beforeEach(() => { - dataSource = new DefaultManagerApiDataSource({ - managerApiUrl: MANAGER_API_BASE_URL, - }) as jest.Mocked; + dataSource = new AxiosManagerApiDataSource({ + managerApiUrl: DEFAULT_MANAGER_API_BASE_URL, + }) as jest.Mocked; service = new DefaultManagerApiService(dataSource); }); @@ -58,7 +58,7 @@ describe("ManagerApiService", () => { describe("error cases", () => { it("should return an error when the data source fails with a known error", async () => { - const error = new FetchError(new Error("Failed to fetch data")); + const error = new HttpFetchApiError(new Error("Failed to fetch data")); dataSource.getAppsByHash.mockRejectedValue(error); expect(await service.getAppsByHash([BTC_APP])).toEqual(Left(error)); }); @@ -67,7 +67,7 @@ describe("ManagerApiService", () => { const error = new Error("unkown error"); dataSource.getAppsByHash.mockRejectedValue(error); expect(await service.getAppsByHash([BTC_APP])).toEqual( - Left(new FetchError(error)), + Left(new HttpFetchApiError(error)), ); }); }); diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts index 6f6250f6e..1de3a3f83 100644 --- a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts +++ b/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts @@ -4,8 +4,8 @@ import { EitherAsync } from "purify-ts"; import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes"; -import { FetchError } from "@internal/manager-api/model/Errors"; -import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; +import { HttpFetchApiError } from "@internal/manager-api/model/Errors"; +import { Application } from "@internal/manager-api/model/ManagerApiType"; import { ManagerApiService } from "./ManagerApiService"; @@ -27,7 +27,7 @@ export class DefaultManagerApiService implements ManagerApiService { return acc; }, []); - return EitherAsync>( + return EitherAsync>( async ({ fromPromise, throwE }) => { if (hashes.length === 0) { return []; @@ -38,11 +38,11 @@ export class DefaultManagerApiService implements ManagerApiService { ); return response; } catch (error) { - if (error instanceof FetchError) { + if (error instanceof HttpFetchApiError) { return throwE(error); } - return throwE(new FetchError(error)); + return throwE(new HttpFetchApiError(error)); } }, ); diff --git a/packages/core/src/internal/manager-api/service/ManagerApiService.ts b/packages/core/src/internal/manager-api/service/ManagerApiService.ts index 06bfa11fe..466702f05 100644 --- a/packages/core/src/internal/manager-api/service/ManagerApiService.ts +++ b/packages/core/src/internal/manager-api/service/ManagerApiService.ts @@ -1,11 +1,11 @@ import { EitherAsync } from "purify-ts"; import { ListAppsResponse } from "@api/command/os/ListAppsCommand"; -import { FetchError } from "@internal/manager-api/model/Errors"; -import { ApplicationEntity } from "@internal/manager-api/model/ManagerApiResponses"; +import { HttpFetchApiError } from "@internal/manager-api/model/Errors"; +import { Application } from "@internal/manager-api/model/ManagerApiType"; export interface ManagerApiService { getAppsByHash( apps: ListAppsResponse, - ): EitherAsync>; + ): EitherAsync>; } diff --git a/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts b/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts index 16bc4baa4..2c8284ed7 100644 --- a/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts +++ b/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts @@ -9,14 +9,14 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { SendApduUseCase } from "@internal/send/use-case/SendApduUseCase"; import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub"; -jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); +jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource"); let logger: LoggerPublisherService; let sessionService: DeviceSessionService; @@ -28,7 +28,7 @@ describe("SendApduUseCase", () => { beforeEach(() => { logger = new DefaultLoggerPublisherService([], "send-apdu-use-case"); sessionService = new DefaultDeviceSessionService(() => logger); - managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiDataSource = new AxiosManagerApiDataSource({ managerApiUrl: "http://fake.url", }); managerApi = new DefaultManagerApiService(managerApiDataSource); diff --git a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts b/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts index 691ed1619..bf9fac291 100644 --- a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts +++ b/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts @@ -3,14 +3,14 @@ import { DefaultDeviceSessionService } from "@internal/device-session/service/De import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; -import { DefaultManagerApiDataSource } from "@internal/manager-api/data/DefaultManagerApiDataSource"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource"; import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { GetConnectedDeviceUseCase } from "@internal/usb/use-case/GetConnectedDeviceUseCase"; import { ConnectedDevice } from "@root/src"; -jest.mock("@internal/manager-api/data/DefaultManagerApiDataSource"); +jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource"); let logger: LoggerPublisherService; let sessionService: DeviceSessionService; @@ -25,7 +25,7 @@ describe("GetConnectedDevice", () => { [], "get-connected-device-use-case", ); - managerApiDataSource = new DefaultManagerApiDataSource({ + managerApiDataSource = new AxiosManagerApiDataSource({ managerApiUrl: "http://fake.url", }); managerApi = new DefaultManagerApiService(managerApiDataSource); From 2a4449444d7fecc874f393b2bfc2942fa6cd7c15 Mon Sep 17 00:00:00 2001 From: jdabbech-ledger Date: Fri, 2 Aug 2024 09:43:48 +0200 Subject: [PATCH 26/46] :sparkles: (keyring-eth): Sign personal message command --- .changeset/seven-beans-poke.md | 5 + .../SignPersonalMessageCommand.test.ts | 244 ++++++++++++++++++ .../command/SignPersonalMessageCommand.ts | 131 +++++++++- 3 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 .changeset/seven-beans-poke.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SignPersonalMessageCommand.test.ts diff --git a/.changeset/seven-beans-poke.md b/.changeset/seven-beans-poke.md new file mode 100644 index 000000000..beb73471a --- /dev/null +++ b/.changeset/seven-beans-poke.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Add SignPersonalMessage command diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignPersonalMessageCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignPersonalMessageCommand.test.ts new file mode 100644 index 000000000..28b6d9648 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignPersonalMessageCommand.test.ts @@ -0,0 +1,244 @@ +import { + ApduResponse, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; +import { Just, Nothing } from "purify-ts"; + +import { SignPersonalMessageCommand } from "./SignPersonalMessageCommand"; + +const DERIVATION_PATH = "44'/60'/0'/0/0"; + +const SIGN_PERSONAL_EMPTY_MESSAGE_APDU = new Uint8Array([ + 0xe0, 0x08, 0x00, 0x00, 0x19, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x00, + 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +]); + +const SIGN_PERSONAL_MESSAGE_INVALID_INDEX_APDU = new Uint8Array([ + 0xe0, 0x08, 0x80, 0x00, 0x00, +]); + +const SIGN_PERSONAL_SHORT_MESSAGE = "test"; +const SIGN_PERSONAL_SHORT_MESSAGE_APDU = new Uint8Array([ + 0xe0, 0x08, 0x00, 0x00, 0x1d, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x00, + 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, +]); + +const SIGN_PERSONAL_LONG_MESSAGE = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut in porta est, non vehicula enim. Etiam leo diam, accumsan ac pretium et, tincidunt in nunc. Quisque faucibus fermentum maximus. Donec non nisi ut erat auctor congue a vehicula neque. Maecenas volutpat lectus vel bibendum mattis. Aenean feugiat nulla diam, vitae interdum lacus ornare ac. Cras posuere, elit at convallis pretium, risus tortor volutpat sapien, eu mollis sapien dolor id sapien. Ut efficitur, ipsum vitae feugiat congue, ex nibh tristique nibh."; + +const SIGN_PERSONAL_LONG_MESSAGE_FIRST_APDU = new Uint8Array([ + 0xe0, 0x08, 0x00, 0x00, 0x96, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x00, + 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x06, 0x4c, 0x6f, 0x72, 0x65, 0x6d, 0x20, 0x69, 0x70, 0x73, + 0x75, 0x6d, 0x20, 0x64, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x73, 0x69, 0x74, 0x20, + 0x61, 0x6d, 0x65, 0x74, 0x2c, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x63, 0x74, + 0x65, 0x74, 0x75, 0x72, 0x20, 0x61, 0x64, 0x69, 0x70, 0x69, 0x73, 0x63, 0x69, + 0x6e, 0x67, 0x20, 0x65, 0x6c, 0x69, 0x74, 0x2e, 0x20, 0x55, 0x74, 0x20, 0x69, + 0x6e, 0x20, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x20, 0x65, 0x73, 0x74, 0x2c, 0x20, + 0x6e, 0x6f, 0x6e, 0x20, 0x76, 0x65, 0x68, 0x69, 0x63, 0x75, 0x6c, 0x61, 0x20, + 0x65, 0x6e, 0x69, 0x6d, 0x2e, 0x20, 0x45, 0x74, 0x69, 0x61, 0x6d, 0x20, 0x6c, + 0x65, 0x6f, 0x20, 0x64, 0x69, 0x61, 0x6d, 0x2c, 0x20, 0x61, 0x63, 0x63, 0x75, + 0x6d, 0x73, 0x61, 0x6e, 0x20, 0x61, 0x63, 0x20, 0x70, 0x72, 0x65, 0x74, +]); + +const SIGN_PERSONAL_LONG_MESSAGE_SECOND_APDU = new Uint8Array([ + 0xe0, 0x08, 0x80, 0x00, 0x96, 0x69, 0x75, 0x6d, 0x20, 0x65, 0x74, 0x2c, 0x20, + 0x74, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x75, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x20, + 0x6e, 0x75, 0x6e, 0x63, 0x2e, 0x20, 0x51, 0x75, 0x69, 0x73, 0x71, 0x75, 0x65, + 0x20, 0x66, 0x61, 0x75, 0x63, 0x69, 0x62, 0x75, 0x73, 0x20, 0x66, 0x65, 0x72, + 0x6d, 0x65, 0x6e, 0x74, 0x75, 0x6d, 0x20, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, + 0x73, 0x2e, 0x20, 0x44, 0x6f, 0x6e, 0x65, 0x63, 0x20, 0x6e, 0x6f, 0x6e, 0x20, + 0x6e, 0x69, 0x73, 0x69, 0x20, 0x75, 0x74, 0x20, 0x65, 0x72, 0x61, 0x74, 0x20, + 0x61, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x20, 0x63, 0x6f, 0x6e, 0x67, 0x75, 0x65, + 0x20, 0x61, 0x20, 0x76, 0x65, 0x68, 0x69, 0x63, 0x75, 0x6c, 0x61, 0x20, 0x6e, + 0x65, 0x71, 0x75, 0x65, 0x2e, 0x20, 0x4d, 0x61, 0x65, 0x63, 0x65, 0x6e, 0x61, + 0x73, 0x20, 0x76, 0x6f, 0x6c, 0x75, 0x74, 0x70, 0x61, 0x74, 0x20, 0x6c, 0x65, + 0x63, 0x74, 0x75, 0x73, 0x20, 0x76, 0x65, 0x6c, 0x20, 0x62, 0x69, 0x62, +]); +const SIGN_PERSONAL_LONG_MESSAGE_THIRD_APDU = new Uint8Array([ + 0xe0, 0x08, 0x80, 0x00, 0x96, 0x65, 0x6e, 0x64, 0x75, 0x6d, 0x20, 0x6d, 0x61, + 0x74, 0x74, 0x69, 0x73, 0x2e, 0x20, 0x41, 0x65, 0x6e, 0x65, 0x61, 0x6e, 0x20, + 0x66, 0x65, 0x75, 0x67, 0x69, 0x61, 0x74, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x61, + 0x20, 0x64, 0x69, 0x61, 0x6d, 0x2c, 0x20, 0x76, 0x69, 0x74, 0x61, 0x65, 0x20, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x64, 0x75, 0x6d, 0x20, 0x6c, 0x61, 0x63, 0x75, + 0x73, 0x20, 0x6f, 0x72, 0x6e, 0x61, 0x72, 0x65, 0x20, 0x61, 0x63, 0x2e, 0x20, + 0x43, 0x72, 0x61, 0x73, 0x20, 0x70, 0x6f, 0x73, 0x75, 0x65, 0x72, 0x65, 0x2c, + 0x20, 0x65, 0x6c, 0x69, 0x74, 0x20, 0x61, 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x76, + 0x61, 0x6c, 0x6c, 0x69, 0x73, 0x20, 0x70, 0x72, 0x65, 0x74, 0x69, 0x75, 0x6d, + 0x2c, 0x20, 0x72, 0x69, 0x73, 0x75, 0x73, 0x20, 0x74, 0x6f, 0x72, 0x74, 0x6f, + 0x72, 0x20, 0x76, 0x6f, 0x6c, 0x75, 0x74, 0x70, 0x61, 0x74, 0x20, 0x73, 0x61, + 0x70, 0x69, 0x65, 0x6e, 0x2c, 0x20, 0x65, 0x75, 0x20, 0x6d, 0x6f, 0x6c, +]); + +const SIGN_PERSONAL_LONG_MESSAGE_FOURTH_APDU = new Uint8Array([ + 0xe0, 0x08, 0x80, 0x00, 0x5d, 0x6c, 0x69, 0x73, 0x20, 0x73, 0x61, 0x70, 0x69, + 0x65, 0x6e, 0x20, 0x64, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x69, 0x64, 0x20, 0x73, + 0x61, 0x70, 0x69, 0x65, 0x6e, 0x2e, 0x20, 0x55, 0x74, 0x20, 0x65, 0x66, 0x66, + 0x69, 0x63, 0x69, 0x74, 0x75, 0x72, 0x2c, 0x20, 0x69, 0x70, 0x73, 0x75, 0x6d, + 0x20, 0x76, 0x69, 0x74, 0x61, 0x65, 0x20, 0x66, 0x65, 0x75, 0x67, 0x69, 0x61, + 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x67, 0x75, 0x65, 0x2c, 0x20, 0x65, 0x78, 0x20, + 0x6e, 0x69, 0x62, 0x68, 0x20, 0x74, 0x72, 0x69, 0x73, 0x74, 0x69, 0x71, 0x75, + 0x65, 0x20, 0x6e, 0x69, 0x62, 0x68, 0x2e, +]); + +const SIGN_PERSONAL_LONG_MESSAGE_APDUS = [ + SIGN_PERSONAL_LONG_MESSAGE_FIRST_APDU, + SIGN_PERSONAL_LONG_MESSAGE_SECOND_APDU, + SIGN_PERSONAL_LONG_MESSAGE_THIRD_APDU, + SIGN_PERSONAL_LONG_MESSAGE_FOURTH_APDU, +]; + +const SIGN_PERSONAL_MESSAGE_SHORT_SUCCESS_RESPONSE = new Uint8Array([ + 0x1b, 0x97, 0xa4, 0xca, 0x8f, 0x69, 0x46, 0x33, 0x59, 0x26, 0x01, 0xf5, 0xa2, + 0x3e, 0x0b, 0xcc, 0x55, 0x3c, 0x9d, 0x0a, 0x90, 0xd3, 0xa3, 0x42, 0x2d, 0x57, + 0x55, 0x08, 0xa9, 0x28, 0x98, 0xb9, 0x6e, 0x69, 0x50, 0xd0, 0x2e, 0x74, 0xe9, + 0xc1, 0x02, 0xc1, 0x64, 0xa2, 0x25, 0x53, 0x30, 0x82, 0xca, 0xbd, 0xd8, 0x90, + 0xef, 0xc4, 0x63, 0xf6, 0x7f, 0x60, 0xce, 0xfe, 0x8c, 0x3f, 0x87, 0xcf, 0xce, +]); + +const SIGN_PERSONAL_LONG_MESSAGE_SUCCESS_RESPONSE = new Uint8Array([ + 0x1b, 0x19, 0x10, 0x0e, 0x53, 0x38, 0xbc, 0x6c, 0x77, 0x20, 0xbb, 0x47, 0xcf, + 0x39, 0x23, 0x7b, 0x4f, 0x27, 0x31, 0x6c, 0xb2, 0xe4, 0xe2, 0xff, 0x00, 0x46, + 0x18, 0xb7, 0x63, 0xc8, 0x6c, 0x8a, 0x06, 0x0f, 0xf0, 0x1a, 0x85, 0x57, 0x18, + 0xd7, 0x97, 0x5c, 0x1c, 0x54, 0xab, 0xcf, 0x7d, 0x32, 0xff, 0x96, 0x30, 0x7c, + 0x0b, 0xda, 0x8d, 0x69, 0x5d, 0x14, 0x29, 0x0d, 0x4b, 0xc5, 0x4d, 0x27, 0x8b, +]); + +describe("SignPersonalMessageCommand", (): void => { + describe("getApdu", () => { + it("should return correct apdu for an empty message", () => { + // given + const command = new SignPersonalMessageCommand({ + derivationPath: DERIVATION_PATH, + message: "", + index: 0, + }); + // when + const apdu = command.getApdu(); + // then + expect(apdu.getRawApdu()).toStrictEqual(SIGN_PERSONAL_EMPTY_MESSAGE_APDU); + }); + it("should return correct apdu for an invalid index", () => { + // given + const command = new SignPersonalMessageCommand({ + derivationPath: DERIVATION_PATH, + message: SIGN_PERSONAL_SHORT_MESSAGE, + index: 42, + }); + // when + const apdu = command.getApdu(); + // then + expect(apdu.getRawApdu()).toStrictEqual( + SIGN_PERSONAL_MESSAGE_INVALID_INDEX_APDU, + ); + }); + it("should return the signPersonalMessage apdu for a short message", () => { + // given + const command = new SignPersonalMessageCommand({ + derivationPath: DERIVATION_PATH, + message: SIGN_PERSONAL_SHORT_MESSAGE, + index: 0, + }); + // when + const apdu = command.getApdu(); + // then + expect(apdu.getRawApdu()).toStrictEqual(SIGN_PERSONAL_SHORT_MESSAGE_APDU); + }); + it.each(SIGN_PERSONAL_LONG_MESSAGE_APDUS)( + "should return correct apdu for a long message at index %#", + (expectedApdu) => { + // given + const command = new SignPersonalMessageCommand({ + derivationPath: DERIVATION_PATH, + message: SIGN_PERSONAL_LONG_MESSAGE, + index: SIGN_PERSONAL_LONG_MESSAGE_APDUS.indexOf(expectedApdu), + }); + // when + const apdu = command.getApdu(); + // then + expect(apdu.getRawApdu()).toStrictEqual(expectedApdu); + }, + ); + }); + + describe("parseResponse", () => { + it("should return correct response after signing success for a short message", () => { + // given + const command = new SignPersonalMessageCommand({ + message: SIGN_PERSONAL_SHORT_MESSAGE, + derivationPath: DERIVATION_PATH, + index: 0, + }); + const apduResponse = new ApduResponse({ + statusCode: new Uint8Array([0x90, 0x00]), + data: SIGN_PERSONAL_MESSAGE_SHORT_SUCCESS_RESPONSE, + }); + // when + const response = command.parseResponse(apduResponse); + // then + expect(response).toStrictEqual( + Just({ + r: "0x97a4ca8f694633592601f5a23e0bcc553c9d0a90d3a3422d575508a92898b96e", + s: "0x6950d02e74e9c102c164a225533082cabdd890efc463f67f60cefe8c3f87cfce", + v: 27, + }), + ); + }); + it("should throw an error if user refused on device", () => { + const command = new SignPersonalMessageCommand({ + message: SIGN_PERSONAL_SHORT_MESSAGE, + derivationPath: DERIVATION_PATH, + index: 0, + }); + const apduResponse = new ApduResponse({ + statusCode: new Uint8Array([0x69, 0x85]), + data: new Uint8Array([]), + }); + // when + const response = () => command.parseResponse(apduResponse); + // then + expect(response).toThrow(InvalidStatusWordError); + }); + it("should return nothing if not last index of a long message", () => { + // given + const command = new SignPersonalMessageCommand({ + derivationPath: DERIVATION_PATH, + message: SIGN_PERSONAL_LONG_MESSAGE, + index: 0, + }); + // when + const response = command.parseResponse( + new ApduResponse({ + statusCode: new Uint8Array([0x90, 0x00]), + data: new Uint8Array([]), + }), + ); + // then + expect(response).toStrictEqual(Nothing); + }); + it("should return correct response of a long message", () => { + // given + const command = new SignPersonalMessageCommand({ + message: SIGN_PERSONAL_LONG_MESSAGE, + derivationPath: DERIVATION_PATH, + index: 0, + }); + const apduResponse = new ApduResponse({ + statusCode: new Uint8Array([0x90, 0x00]), + data: SIGN_PERSONAL_LONG_MESSAGE_SUCCESS_RESPONSE, + }); + // when + const response = command.parseResponse(apduResponse); + // then + expect(response).toStrictEqual( + Just({ + r: "0x19100e5338bc6c7720bb47cf39237b4f27316cb2e4e2ff004618b763c86c8a06", + s: "0x0ff01a855718d7975c1c54abcf7d32ff96307c0bda8d695d14290d4bc54d278b", + v: 27, + }), + ); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignPersonalMessageCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignPersonalMessageCommand.ts index 4bbb9bb19..c3d29f06a 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SignPersonalMessageCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignPersonalMessageCommand.ts @@ -1,2 +1,131 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#sign-eth-personal-message -export class SignPersonalMessageCommand {} +import { + Apdu, + ApduBuilder, + ApduBuilderArgs, + ApduParser, + ApduResponse, + Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; +import { Just, Maybe, Nothing } from "purify-ts"; + +import { Signature } from "@api/model/Signature"; +import { DerivationPathUtils } from "@internal/shared/utils/DerivationPathUtils"; + +const MAX_CHUNK_SIZE = 150; +const PATH_SIZE = 4; +const MESSAGE_LENGTH_SIZE = 4; +const DERIVATIONS_COUNT_SIZE = 1; +const R_LENGTH = 32; +const S_LENGTH = 32; + +export type SignPersonalMessageCommandArgs = { + /** + * The derivation path to use to sign the transaction. + */ + readonly derivationPath: string; + /** + * The complete serialized transaction data. + */ + readonly message: string; + /** + * The index of the chunk to sign. + */ + readonly index: number; +}; + +export type SignPersonalMessageCommandResponse = Maybe; + +export class SignPersonalMessageCommand + implements + Command +{ + readonly args: SignPersonalMessageCommandArgs; + + constructor(args: SignPersonalMessageCommandArgs) { + this.args = args; + } + + getApdu(): Apdu { + const { derivationPath, message, index } = this.args; + const signPersonalMessageArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x08, + p1: index === 0 ? 0x00 : 0x80, + p2: 0x00, + }; + const paths = DerivationPathUtils.splitPath(derivationPath); + const builder = new ApduBuilder(signPersonalMessageArgs); + const messageFirstChunkIndex = + MAX_CHUNK_SIZE - + paths.length * PATH_SIZE - + DERIVATIONS_COUNT_SIZE - + MESSAGE_LENGTH_SIZE; + + if (index === 0) { + // add derivation paths count to the first packet + builder.add8BitUIntToData(paths.length); + // add every derivation path + paths.forEach((path) => { + builder.add32BitUIntToData(path); + }); + // add message length + builder.add32BitUIntToData(message.length); + // add 150 bytes of data minus the count of derivation, the path size and the message length + builder.addAsciiStringToData(message.slice(0, messageFirstChunkIndex)); + } else { + // add 150 bytes of data starting from the second packet + builder.addAsciiStringToData( + message.slice( + messageFirstChunkIndex + (index - 1) * MAX_CHUNK_SIZE, + messageFirstChunkIndex + index * MAX_CHUNK_SIZE, + ), + ); + } + return builder.build(); + } + + parseResponse( + apduResponse: ApduResponse, + ): SignPersonalMessageCommandResponse { + const parser = new ApduParser(apduResponse); + + if (!CommandUtils.isSuccessResponse(apduResponse)) { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + apduResponse.statusCode, + )}`, + ); + } + + // The data is returned only for the last chunk + const v = parser.extract8BitUInt(); + if (!v) { + return Nothing; + } + + const r = parser.encodeToHexaString( + parser.extractFieldByLength(R_LENGTH), + true, + ); + if (!r) { + throw new InvalidStatusWordError("R is missing"); + } + + const s = parser.encodeToHexaString( + parser.extractFieldByLength(S_LENGTH), + true, + ); + if (!s) { + throw new InvalidStatusWordError("S is missing"); + } + + return Just({ + r, + s, + v, + }); + } +} From 38f0ce3288c7bb199cb82ea442b72f2b871bff9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Aug 2024 13:38:51 +0000 Subject: [PATCH 27/46] :arrow_up: (repo) [NO-ISSUE]: Bump autoprefixer from 10.4.19 to 10.4.20 Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.4.19 to 10.4.20. - [Release notes](https://github.com/postcss/autoprefixer/releases) - [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/autoprefixer/compare/10.4.19...10.4.20) --- updated-dependencies: - dependency-name: autoprefixer dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- apps/sample/package.json | 2 +- pnpm-lock.yaml | 97 ++++++++++++++-------------------------- 2 files changed, 35 insertions(+), 64 deletions(-) diff --git a/apps/sample/package.json b/apps/sample/package.json index 8fb27e123..144a26a9c 100644 --- a/apps/sample/package.json +++ b/apps/sample/package.json @@ -28,7 +28,7 @@ "@ledgerhq/tsconfig-dsdk": "workspace:*", "@types/react": "^18.3.3", "@types/styled-components": "^5.1.25", - "autoprefixer": "^10.4.19", + "autoprefixer": "^10.4.20", "globals": "15.8.0", "postcss": "^8.4.38", "typescript": "5.4.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c9c9c10d..cd648ddb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,8 +103,8 @@ importers: specifier: ^5.1.25 version: 5.1.34 autoprefixer: - specifier: ^10.4.19 - version: 10.4.19(postcss@8.4.38) + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.38) globals: specifier: 15.8.0 version: 15.8.0 @@ -2704,8 +2704,8 @@ packages: atomically@2.0.2: resolution: {integrity: sha512-Xfmb4q5QV7uqTlVdMSTtO5eF4DCHfNOdaPyKlbFShkzeNP+3lj3yjjcbdjSmEY4+pDBKJ9g26aP+ImTe88UHoQ==} - autoprefixer@10.4.19: - resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -2824,13 +2824,8 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - browserslist@4.23.1: - resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2904,12 +2899,12 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001607: - resolution: {integrity: sha512-WcvhVRjXLKFB/kmOFVwELtMxyhq3iM/MvmXcyCe2PNf166c39mptscOc/45TTS96n2gpNV2z7+NakArTWZCQ3w==} - caniuse-lite@1.0.30001633: resolution: {integrity: sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==} + caniuse-lite@1.0.30001647: + resolution: {integrity: sha512-n83xdNiyeNcHpzWY+1aFbqCK7LuLfBricc4+alSQL2Xb6OR3XpnQAmlDG+pQcdTfiHRuLcQ96VOfrPSGiNJYSg==} + chalk@2.3.0: resolution: {integrity: sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==} engines: {node: '>=4'} @@ -3340,11 +3335,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.4.729: - resolution: {integrity: sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==} - - electron-to-chromium@1.4.801: - resolution: {integrity: sha512-PnlUz15ii38MZMD2/CEsAzyee8tv9vFntX5nhtd2/4tv4HqY7C5q2faUAjmkXS/UFpVooJ/5H6kayRKYWoGMXQ==} + electron-to-chromium@1.5.4: + resolution: {integrity: sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==} elliptic@6.5.4: resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} @@ -4897,8 +4889,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} node-stream-zip@1.15.0: resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} @@ -6153,14 +6145,8 @@ packages: unplugin@1.0.1: resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} - update-browserslist-db@1.0.13: - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - update-browserslist-db@1.0.16: - resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -6520,7 +6506,7 @@ snapshots: dependencies: '@babel/compat-data': 7.23.5 '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 + browserslist: 4.23.3 lru-cache: 5.1.1 semver: 6.3.1 @@ -6528,7 +6514,7 @@ snapshots: dependencies: '@babel/compat-data': 7.24.7 '@babel/helper-validator-option': 7.24.7 - browserslist: 4.23.1 + browserslist: 4.23.3 lru-cache: 5.1.1 semver: 6.3.1 @@ -9927,13 +9913,13 @@ snapshots: stubborn-fs: 1.2.5 when-exit: 2.1.2 - autoprefixer@10.4.19(postcss@8.4.38): + autoprefixer@10.4.20(postcss@8.4.38): dependencies: - browserslist: 4.23.0 - caniuse-lite: 1.0.30001607 + browserslist: 4.23.3 + caniuse-lite: 1.0.30001647 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 postcss: 8.4.38 postcss-value-parser: 4.2.0 @@ -10102,19 +10088,12 @@ snapshots: brorand@1.1.0: {} - browserslist@4.23.0: - dependencies: - caniuse-lite: 1.0.30001607 - electron-to-chromium: 1.4.729 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) - - browserslist@4.23.1: + browserslist@4.23.3: dependencies: - caniuse-lite: 1.0.30001633 - electron-to-chromium: 1.4.801 - node-releases: 2.0.14 - update-browserslist-db: 1.0.16(browserslist@4.23.1) + caniuse-lite: 1.0.30001647 + electron-to-chromium: 1.5.4 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) bs-logger@0.2.6: dependencies: @@ -10184,10 +10163,10 @@ snapshots: camelize@1.0.1: {} - caniuse-lite@1.0.30001607: {} - caniuse-lite@1.0.30001633: {} + caniuse-lite@1.0.30001647: {} + chalk@2.3.0: dependencies: ansi-styles: 3.2.1 @@ -10423,7 +10402,7 @@ snapshots: core-js-compat@3.37.1: dependencies: - browserslist: 4.23.1 + browserslist: 4.23.3 core-js@3.37.1: {} @@ -10684,9 +10663,7 @@ snapshots: dependencies: jake: 10.8.7 - electron-to-chromium@1.4.729: {} - - electron-to-chromium@1.4.801: {} + electron-to-chromium@1.5.4: {} elliptic@6.5.4: dependencies: @@ -12587,7 +12564,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.14: {} + node-releases@2.0.18: {} node-stream-zip@1.15.0: {} @@ -13853,15 +13830,9 @@ snapshots: webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 - update-browserslist-db@1.0.13(browserslist@4.23.0): - dependencies: - browserslist: 4.23.0 - escalade: 3.1.2 - picocolors: 1.0.1 - - update-browserslist-db@1.0.16(browserslist@4.23.1): + update-browserslist-db@1.1.0(browserslist@4.23.3): dependencies: - browserslist: 4.23.1 + browserslist: 4.23.3 escalade: 3.1.2 picocolors: 1.0.1 @@ -13948,7 +13919,7 @@ snapshots: '@webassemblyjs/wasm-parser': 1.12.1 acorn: 8.12.1 acorn-import-attributes: 1.9.5(acorn@8.12.1) - browserslist: 4.23.1 + browserslist: 4.23.3 chrome-trace-event: 1.0.4 enhanced-resolve: 5.17.0 es-module-lexer: 1.5.4 From dd605979f24b6a95bc0fecdecc5586d46cb05d6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 07:51:03 +0000 Subject: [PATCH 28/46] :arrow_up: (repo) [NO-ISSUE]: Bump @tsconfig/recommended Bumps [@tsconfig/recommended](https://github.com/tsconfig/bases/tree/HEAD/bases) from 1.0.6 to 1.0.7. - [Commits](https://github.com/tsconfig/bases/commits/HEAD/bases) --- updated-dependencies: - dependency-name: "@tsconfig/recommended" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- packages/config/typescript/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json index 2618a95f9..30ad45dd0 100644 --- a/packages/config/typescript/package.json +++ b/packages/config/typescript/package.json @@ -7,7 +7,7 @@ "web.json" ], "devDependencies": { - "@tsconfig/recommended": "^1.0.6", + "@tsconfig/recommended": "^1.0.7", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd648ddb9..3c32f60b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,8 +148,8 @@ importers: packages/config/typescript: devDependencies: '@tsconfig/recommended': - specifier: ^1.0.6 - version: 1.0.6 + specifier: ^1.0.7 + version: 1.0.7 '@types/react': specifier: ^18.3.3 version: 18.3.3 @@ -2300,8 +2300,8 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@tsconfig/recommended@1.0.6': - resolution: {integrity: sha512-0IKu9GHYF1NGTJiYgfWwqnOQSlnE9V9R7YohHNNf0/fj/SyOZWzdd06JFr0fLpg1Mqw0kGbYg8w5xdkSqLKM9g==} + '@tsconfig/recommended@1.0.7': + resolution: {integrity: sha512-xiNMgCuoy4mCL4JTywk9XFs5xpRUcKxtWEcMR6FNMtsgewYTIgIR+nvlP4A4iRCAzRsHMnPhvTRrzp4AGcRTEA==} '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -9449,7 +9449,7 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@tsconfig/recommended@1.0.6': {} + '@tsconfig/recommended@1.0.7': {} '@types/babel__core@7.20.5': dependencies: From a31c29851efd6b4eca4503a17b04857788f003b9 Mon Sep 17 00:00:00 2001 From: jiyuzhuang Date: Mon, 5 Aug 2024 18:21:36 +0200 Subject: [PATCH 29/46] =?UTF-8?q?=E2=9C=A8=20(keyring-eth):=20Implement=20?= =?UTF-8?q?SetPluginCommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/blue-pandas-wash.md | 5 ++ .../command/SetPluginCommand.test.ts | 72 +++++++++++++++++++ .../app-binder/command/SetPluginCommand.ts | 66 ++++++++++++++++- 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 .changeset/blue-pandas-wash.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SetPluginCommand.test.ts diff --git a/.changeset/blue-pandas-wash.md b/.changeset/blue-pandas-wash.md new file mode 100644 index 000000000..f27aa75b3 --- /dev/null +++ b/.changeset/blue-pandas-wash.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement SetPluginCommand diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SetPluginCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SetPluginCommand.test.ts new file mode 100644 index 000000000..821980886 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SetPluginCommand.test.ts @@ -0,0 +1,72 @@ +import { + ApduResponse, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +import { SetPluginCommand, SetPluginCommandArgs } from "./SetPluginCommand"; + +const SET_PLUGIN_COMMAND_PAYLOAD = + "010106455243373231c5b07a55501014f36ec5d39d950a321439f6dd7642842e0e0000000000000001020147304502206d9f515916283e08fa6cdab205668c0739c558dcd6691a69ce74cd89fbc2cc6e022100c28c17b058e6d453570a58d69ff62042037dc61149af2f5161d5c36fdc5dc301"; + +const SET_PLUGIN_COMMAND_APDU = Uint8Array.from([ + 0xe0, 0x16, 0x00, 0x00, 0x73, 0x01, 0x01, 0x06, 0x45, 0x52, 0x43, 0x37, 0x32, + 0x31, 0xc5, 0xb0, 0x7a, 0x55, 0x50, 0x10, 0x14, 0xf3, 0x6e, 0xc5, 0xd3, 0x9d, + 0x95, 0x0a, 0x32, 0x14, 0x39, 0xf6, 0xdd, 0x76, 0x42, 0x84, 0x2e, 0x0e, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x01, 0x47, 0x30, 0x45, 0x02, + 0x20, 0x6d, 0x9f, 0x51, 0x59, 0x16, 0x28, 0x3e, 0x08, 0xfa, 0x6c, 0xda, 0xb2, + 0x05, 0x66, 0x8c, 0x07, 0x39, 0xc5, 0x58, 0xdc, 0xd6, 0x69, 0x1a, 0x69, 0xce, + 0x74, 0xcd, 0x89, 0xfb, 0xc2, 0xcc, 0x6e, 0x02, 0x21, 0x00, 0xc2, 0x8c, 0x17, + 0xb0, 0x58, 0xe6, 0xd4, 0x53, 0x57, 0x0a, 0x58, 0xd6, 0x9f, 0xf6, 0x20, 0x42, + 0x03, 0x7d, 0xc6, 0x11, 0x49, 0xaf, 0x2f, 0x51, 0x61, 0xd5, 0xc3, 0x6f, 0xdc, + 0x5d, 0xc3, 0x01, +]); + +describe("SetPluginCommand", () => { + describe("getApdu", () => { + it("returns the correct APDU", () => { + // GIVEN + const args: SetPluginCommandArgs = { + data: SET_PLUGIN_COMMAND_PAYLOAD, + }; + // WHEN + const command = new SetPluginCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(SET_PLUGIN_COMMAND_APDU); + }); + }); + describe("parseResponse", () => { + it("should throw an error if the response status code is invalid", () => { + // GIVEN + const invalidStatusCodes = [ + [0x6a, 0x80], + [0x69, 0x84], + [0x6d, 0x00], + ]; + for (const statusCode of invalidStatusCodes) { + const response: ApduResponse = { + data: Buffer.from([]), + statusCode: Buffer.from(statusCode), // Invalid status code + }; + // WHEN + const command = new SetPluginCommand({ data: "" }); + // THEN + expect(() => command.parseResponse(response)).toThrow( + InvalidStatusWordError, + ); + } + }); + + it("should not throw if the response status code is correct", () => { + // GIVEN + const response: ApduResponse = { + data: Buffer.from([]), + statusCode: Buffer.from([0x90, 0x00]), // Success status code + }; + // WHEN + const command = new SetPluginCommand({ data: "" }); + // THEN + expect(() => command.parseResponse(response)).not.toThrow(); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SetPluginCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SetPluginCommand.ts index bb22716aa..1385ae19a 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SetPluginCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SetPluginCommand.ts @@ -1,2 +1,66 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#set-plugin -export class SetPluginCommand {} +import { + Apdu, + ApduBuilder, + type ApduBuilderArgs, + ApduParser, + ApduResponse, + type Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +export type SetPluginCommandArgs = { + /** + * The stringified hexa representation of the plugin signature. + */ + data: string; +}; + +export class SetPluginCommand implements Command { + constructor(private args: SetPluginCommandArgs) {} + + getApdu(): Apdu { + const apduBuilderArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x16, + p1: 0x00, + p2: 0x00, + }; + + return new ApduBuilder(apduBuilderArgs) + .addHexaStringToData(this.args.data) + .build(); + } + + parseResponse(response: ApduResponse): void { + const parser = new ApduParser(response); + + // TODO: handle the error correctly using a generic error handler. These error status codes come from the LL implementation, just for backup for now. + if (!CommandUtils.isSuccessResponse(response)) { + if (response.statusCode[0] === 0x6a && response.statusCode[1] === 0x80) { + throw new InvalidStatusWordError( + "The plugin name is too short or too long", + ); + } else if ( + response.statusCode[0] === 0x69 && + response.statusCode[1] === 0x84 + ) { + throw new InvalidStatusWordError( + "the requested plugin is not installed on the device", + ); + } else if ( + response.statusCode[0] === 0x6d && + response.statusCode[1] === 0x00 + ) { + throw new InvalidStatusWordError("ETH app is not up to date"); + } else { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + response.statusCode, + )}`, + ); + } + } + } +} From 5c68f48c25edde30416598bb2152ba5077820724 Mon Sep 17 00:00:00 2001 From: jiyuzhuang Date: Mon, 5 Aug 2024 15:15:07 +0200 Subject: [PATCH 30/46] =?UTF-8?q?=E2=9C=A8=20(keyring-eth):=20Implement=20?= =?UTF-8?q?ProvideNFTInformationCommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/good-oranges-move.md | 5 ++ .../ProvideNFTInformationCommand.test.ts | 69 +++++++++++++++++++ .../command/ProvideNFTInformationCommand.ts | 55 ++++++++++++++- 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 .changeset/good-oranges-move.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/ProvideNFTInformationCommand.test.ts diff --git a/.changeset/good-oranges-move.md b/.changeset/good-oranges-move.md new file mode 100644 index 000000000..0818b4949 --- /dev/null +++ b/.changeset/good-oranges-move.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement ProvideNFTInformationCommand diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideNFTInformationCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideNFTInformationCommand.test.ts new file mode 100644 index 000000000..7cfa21176 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideNFTInformationCommand.test.ts @@ -0,0 +1,69 @@ +import { + ApduResponse, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +import { + ProvideNFTInformationCommand, + ProvideNFTInformationCommandArgs, +} from "./ProvideNFTInformationCommand"; + +const NFT_INFORMATION_PAYLOAD = + "0101084d6574614d6f6a61c5b07a55501014f36ec5d39d950a321439f6dd7600000000000000010101473045022100d5f96cad91b83da224c94945e4c8aeb54f089f52c87302af54f0b6b74159f76a02201a1204a36b15f2ff31149fd05502ad65ee98fe77f30a3c8d9b32eb6cf08cabea"; + +const NFT_INFORMATION_APDU = Uint8Array.from([ + 0xe0, 0x14, 0x00, 0x00, 0x71, 0x01, 0x01, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x4d, + 0x6f, 0x6a, 0x61, 0xc5, 0xb0, 0x7a, 0x55, 0x50, 0x10, 0x14, 0xf3, 0x6e, 0xc5, + 0xd3, 0x9d, 0x95, 0x0a, 0x32, 0x14, 0x39, 0xf6, 0xdd, 0x76, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x47, 0x30, 0x45, 0x02, 0x21, 0x00, + 0xd5, 0xf9, 0x6c, 0xad, 0x91, 0xb8, 0x3d, 0xa2, 0x24, 0xc9, 0x49, 0x45, 0xe4, + 0xc8, 0xae, 0xb5, 0x4f, 0x08, 0x9f, 0x52, 0xc8, 0x73, 0x02, 0xaf, 0x54, 0xf0, + 0xb6, 0xb7, 0x41, 0x59, 0xf7, 0x6a, 0x02, 0x20, 0x1a, 0x12, 0x04, 0xa3, 0x6b, + 0x15, 0xf2, 0xff, 0x31, 0x14, 0x9f, 0xd0, 0x55, 0x02, 0xad, 0x65, 0xee, 0x98, + 0xfe, 0x77, 0xf3, 0x0a, 0x3c, 0x8d, 0x9b, 0x32, 0xeb, 0x6c, 0xf0, 0x8c, 0xab, + 0xea, +]); + +describe("ProvideNFTInformationCommand", () => { + describe("getApdu", () => { + it("should return the raw APDU", () => { + // GIVEN + const args: ProvideNFTInformationCommandArgs = { + data: NFT_INFORMATION_PAYLOAD, + }; + // WHEN + const command = new ProvideNFTInformationCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(NFT_INFORMATION_APDU); + }); + }); + + describe("parseResponse", () => { + it("should throw an error if the response status code is invalid", () => { + // GIVEN + const response: ApduResponse = { + data: Buffer.from([]), + statusCode: Buffer.from([0x6d, 0x00]), // Invalid status code + }; + // WHEN + const command = new ProvideNFTInformationCommand({ data: "" }); + // THEN + expect(() => command.parseResponse(response)).toThrow( + InvalidStatusWordError, + ); + }); + + it("should not throw if the response status code is correct", () => { + // GIVEN + const response: ApduResponse = { + data: Buffer.from([]), + statusCode: Buffer.from([0x90, 0x00]), // Success status code + }; + // WHEN + const command = new ProvideNFTInformationCommand({ data: "" }); + // THEN + expect(() => command.parseResponse(response)).not.toThrow(); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideNFTInformationCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideNFTInformationCommand.ts index 06a11abf5..e9d1fdcbe 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideNFTInformationCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideNFTInformationCommand.ts @@ -1,2 +1,55 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#provide-nft-information -export class ProvideNFTInformationCommand {} +import { + Apdu, + ApduBuilder, + type ApduBuilderArgs, + ApduParser, + ApduResponse, + type Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +export type ProvideNFTInformationCommandArgs = { + /** + * The stringified hexa representation of the NFT data. + */ + data: string; +}; + +export class ProvideNFTInformationCommand + implements Command +{ + constructor(private args: ProvideNFTInformationCommandArgs) {} + + getApdu(): Apdu { + const apduBuilderArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x14, + p1: 0x00, + p2: 0x00, + }; + + return new ApduBuilder(apduBuilderArgs) + .addHexaStringToData(this.args.data) + .build(); + } + + parseResponse(response: ApduResponse): void { + const parser = new ApduParser(response); + + if (response.statusCode[0] === 0x6d && response.statusCode[1] === 0x00) { + // This is temporary, a new error class should be created to handle this case later. + throw new InvalidStatusWordError("ETH app is not up to date"); + } + + if (!CommandUtils.isSuccessResponse(response)) { + // TODO: handle the error correctly using a generic error handler + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + response.statusCode, + )}`, + ); + } + } +} From 6167f44ca9ab4aa90ee0d71af4c13a9f2140bfbb Mon Sep 17 00:00:00 2001 From: jiyuzhuang Date: Fri, 2 Aug 2024 14:54:17 +0200 Subject: [PATCH 31/46] =?UTF-8?q?=F0=9F=90=9B=20(keyring-eth):=20Fix=20get?= =?UTF-8?q?Apdu=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/ProvideDomainNameCommand.test.ts | 23 +++++++++++++++---- .../command/ProvideDomainNameCommand.ts | 15 ++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts index bd62597b9..d79f29e2b 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts @@ -9,40 +9,55 @@ import { } from "./ProvideDomainNameCommand"; const FIRST_CHUNK_APDU = Uint8Array.from([ - 0xe0, 0x22, 0x01, 0x00, 0x06, 0x4c, 0x65, 0x64, 0x67, 0x65, 0x72, + 0xe0, 0x22, 0x01, 0x00, 0x08, 0x00, 0x06, 0x4c, 0x65, 0x64, 0x67, 0x65, 0x72, ]); describe("ProvideDomainNameCommand", () => { describe("getApdu", () => { it("should return the raw APDU", () => { + // GIVEN const args: ProvideDomainNameCommandArgs = { - data: "4C6564676572", + data: "00064C6564676572", index: 0, }; + // WHEN const command = new ProvideDomainNameCommand(args); const apdu = command.getApdu(); + // THEN expect(apdu.getRawApdu()).toStrictEqual(FIRST_CHUNK_APDU); }); }); describe("parseResponse", () => { it("should throw an error if the response status code is invalid", () => { + // GIVEN const response: ApduResponse = { data: Buffer.from([]), statusCode: Buffer.from([0x6a, 0x80]), // Invalid status code }; - const command = new ProvideDomainNameCommand({ data: "", index: 0 }); + // WHEN + const command = new ProvideDomainNameCommand({ + data: "", + index: 0, + }); + // THEN expect(() => command.parseResponse(response)).toThrow( InvalidStatusWordError, ); }); it("should not throw if the response status code is correct", () => { + // GIVEN const response: ApduResponse = { data: Buffer.from([]), statusCode: Buffer.from([0x90, 0x00]), // Success status code }; - const command = new ProvideDomainNameCommand({ data: "", index: 0 }); + // WHEN + const command = new ProvideDomainNameCommand({ + data: "", + index: 0, + }); + // THEN expect(() => command.parseResponse(response)).not.toThrow(); }); }); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts index 671bd37ed..93fb189d5 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts @@ -2,6 +2,7 @@ import { Apdu, ApduBuilder, + type ApduBuilderArgs, ApduParser, ApduResponse, type Command, @@ -11,8 +12,9 @@ import { export type ProvideDomainNameCommandArgs = { /** - * The stringified hexa representation of the domain name. - * @example "4C6564676572" (hexa for "Ledger") + * The chunk of the stringified hexa representation of the domain name prefixed by its length in two bytes. + * If the index equals 0, the first two bytes are the length of the domain name, else all the bytes are the chunk data. + * @example "00064C6564676572" (hexa for "Ledger", first chunk and only chunk) */ data: string; /** @@ -24,14 +26,17 @@ export type ProvideDomainNameCommandArgs = { /** * The command that provides a chunk of the domain name to the device. */ -export class ProvideDomainNameCommand implements Command { +export class ProvideDomainNameCommand + implements Command +{ constructor(private args: ProvideDomainNameCommandArgs) {} getApdu(): Apdu { - const apduBuilderArgs = { + const isFirstChunk = this.args.index === 0; + const apduBuilderArgs: ApduBuilderArgs = { cla: 0xe0, ins: 0x22, - p1: this.args.index === 0 ? 0x01 : 0x00, + p1: isFirstChunk ? 0x01 : 0x00, p2: 0x00, }; From 0a407e37a582bd203557224e0929e8c59005d2cb Mon Sep 17 00:00:00 2001 From: jdabbech-ledger Date: Fri, 9 Aug 2024 09:15:35 +0200 Subject: [PATCH 32/46] :green_heart: (othr): Fix ci builds false positive --- packages/core/scripts/build.mjs | 1 + packages/signer/context-module/scripts/build.mjs | 1 + packages/signer/keyring-eth/scripts/build.mjs | 1 + packages/trusted-apps/scripts/build.mjs | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/core/scripts/build.mjs b/packages/core/scripts/build.mjs index ac064aee1..d80548df4 100644 --- a/packages/core/scripts/build.mjs +++ b/packages/core/scripts/build.mjs @@ -19,4 +19,5 @@ const run = async () => await Promise.all([buildEsm(), builCjs()]); run().catch((e) => { console.error(e); + process.exitCode = e.exitCode; }); diff --git a/packages/signer/context-module/scripts/build.mjs b/packages/signer/context-module/scripts/build.mjs index ac064aee1..d80548df4 100644 --- a/packages/signer/context-module/scripts/build.mjs +++ b/packages/signer/context-module/scripts/build.mjs @@ -19,4 +19,5 @@ const run = async () => await Promise.all([buildEsm(), builCjs()]); run().catch((e) => { console.error(e); + process.exitCode = e.exitCode; }); diff --git a/packages/signer/keyring-eth/scripts/build.mjs b/packages/signer/keyring-eth/scripts/build.mjs index b319316e1..c58b550c9 100644 --- a/packages/signer/keyring-eth/scripts/build.mjs +++ b/packages/signer/keyring-eth/scripts/build.mjs @@ -19,4 +19,5 @@ const run = async () => Promise.all([buildEsm(), builCjs()]); run().catch((e) => { console.error(e); + process.exitCode = e.exitCode; }); diff --git a/packages/trusted-apps/scripts/build.mjs b/packages/trusted-apps/scripts/build.mjs index 0b920a623..d1d48a90a 100644 --- a/packages/trusted-apps/scripts/build.mjs +++ b/packages/trusted-apps/scripts/build.mjs @@ -19,4 +19,5 @@ const run = async () => await Promise.all([buildEsm(), builCjs()]); run().catch((e) => { console.error(e); + process.exitCode = e.exitCode; }); From fd253248a05504d4f2a3b7310bd62132c12d05f7 Mon Sep 17 00:00:00 2001 From: jdabbech-ledger Date: Tue, 6 Aug 2024 13:57:04 +0200 Subject: [PATCH 33/46] :sparkles: (keyring-eth): Add SetExternalPlugin command --- .changeset/old-cups-cheat.md | 5 ++ .../command/SetExternalPluginCommand.test.ts | 85 +++++++++++++++++++ .../command/SetExternalPluginCommand.ts | 62 +++++++++++++- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 .changeset/old-cups-cheat.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SetExternalPluginCommand.test.ts diff --git a/.changeset/old-cups-cheat.md b/.changeset/old-cups-cheat.md new file mode 100644 index 000000000..f73b644d9 --- /dev/null +++ b/.changeset/old-cups-cheat.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Add Set External Plugin command diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SetExternalPluginCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SetExternalPluginCommand.test.ts new file mode 100644 index 000000000..72f208705 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SetExternalPluginCommand.test.ts @@ -0,0 +1,85 @@ +import { + ApduResponse, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +import { SetExternalPluginCommand } from "@internal/app-binder/command/SetExternalPluginCommand"; + +/** Test payload contains: + * Length of plugin name : 08 + * Plugin Name : Paraswap + * contract address: 0xdef171fe48cf0115b1d80b88dc8eab59176fee57 + * method selector: 0xa9059cbb + * **/ +const SET_EXTERNAL_PLUGIN_PAYLOAD = [ + 0x08, 0x50, 0x61, 0x72, 0x61, 0x73, 0x77, 0x61, 0x70, 0xde, 0xf1, 0x71, 0xfe, + 0x48, 0xcf, 0x01, 0x15, 0xb1, 0xd8, 0x0b, 0x88, 0xdc, 0x8e, 0xab, 0x59, 0x17, + 0x6f, 0xee, 0x57, 0xa9, 0x05, 0x9c, 0xbb, +]; +// Public signature key: https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#set-external-plugin +const SET_EXTERNAL_PLUGIN_SIGNATURE = [ + 0x04, 0x82, 0xbb, 0xf2, 0xf3, 0x4f, 0x36, 0x7b, 0x2e, 0x5b, 0xc2, 0x18, 0x47, + 0xb6, 0x56, 0x6f, 0x21, 0xf0, 0x97, 0x6b, 0x22, 0xd3, 0x38, 0x8a, 0x9a, 0x5e, + 0x44, 0x6a, 0xc6, 0x2d, 0x25, 0xcf, 0x72, 0x5b, 0x62, 0xa2, 0x55, 0x5b, 0x2d, + 0xd4, 0x64, 0xa4, 0xda, 0x0a, 0xb2, 0xf4, 0xd5, 0x06, 0x82, 0x05, 0x43, 0xaf, + 0x1d, 0x24, 0x24, 0x70, 0xb1, 0xb1, 0xa9, 0x69, 0xa2, 0x75, 0x78, 0xf3, 0x53, +]; +const SET_EXTERNAL_PLUGIN_APDU = [ + 0xe0, + 0x12, + 0x00, + 0x00, + SET_EXTERNAL_PLUGIN_PAYLOAD.length + SET_EXTERNAL_PLUGIN_SIGNATURE.length, + ...SET_EXTERNAL_PLUGIN_PAYLOAD, + ...SET_EXTERNAL_PLUGIN_SIGNATURE, +]; + +describe("Set External plugin", () => { + describe("getApdu", () => { + it("should retrieve correct apdu", () => { + // given + const command = new SetExternalPluginCommand({ + payload: Uint8Array.from(SET_EXTERNAL_PLUGIN_PAYLOAD), + signature: Uint8Array.from(SET_EXTERNAL_PLUGIN_SIGNATURE), + }); + // when + const apdu = command.getApdu(); + // then + expect(apdu.getRawApdu()).toStrictEqual( + Uint8Array.from(SET_EXTERNAL_PLUGIN_APDU), + ); + }); + }); + describe("parseResponse", () => { + it("should throw an error if status is invalid", () => { + // given + const command = new SetExternalPluginCommand({ + payload: Uint8Array.from([]), + signature: Uint8Array.from([]), + }); + // when + const apduResponse = new ApduResponse({ + statusCode: Uint8Array.from([0x6a, 0x80]), + data: Uint8Array.from([]), + }); + // then + expect(() => command.parseResponse(apduResponse)).toThrowError( + InvalidStatusWordError, + ); + }); + it("should return void if status is success", () => { + // given + const command = new SetExternalPluginCommand({ + payload: Uint8Array.from([]), + signature: Uint8Array.from([]), + }); + // when + const apduResponse = new ApduResponse({ + statusCode: Uint8Array.from([0x90, 0x00]), + data: Uint8Array.from([]), + }); + // then + expect(command.parseResponse(apduResponse)).toBe(void 0); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SetExternalPluginCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SetExternalPluginCommand.ts index faa2579b7..dd7bc3171 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SetExternalPluginCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SetExternalPluginCommand.ts @@ -1,2 +1,62 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#set-external-plugin -export class SetExternalPluginCommand {} + +import { + Apdu, + ApduBuilder, + ApduBuilderArgs, + ApduParser, + ApduResponse, + Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +type SetExternalPluginCommandArgs = { + payload: Uint8Array; + signature: Uint8Array; +}; + +export class SetExternalPluginCommand + implements Command +{ + constructor(private readonly args: SetExternalPluginCommandArgs) {} + + getApdu(): Apdu { + const { payload, signature } = this.args; + const setExternalPluginBuilderArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x12, + p1: 0x00, + p2: 0x00, + }; + const builder = new ApduBuilder(setExternalPluginBuilderArgs); + builder.addBufferToData(payload); + builder.addBufferToData(signature); + + return builder.build(); + } + + parseResponse(apduResponse: ApduResponse): void { + if (CommandUtils.isSuccessResponse(apduResponse)) { + return; + } + + const parser = new ApduParser(apduResponse); + const statusCodeHex = parser.encodeToHexaString(apduResponse.statusCode); + + switch (statusCodeHex) { + case "6a80": + throw new InvalidStatusWordError("Invalid plugin name size"); + case "6984": + throw new InvalidStatusWordError("Plugin not installed on device"); + case "6d00": + throw new InvalidStatusWordError("Version of Eth app not supported"); + default: + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + apduResponse.statusCode, + )}`, + ); + } + } +} From 501812904cbb7eb519651b4c8dbb613198e1e89c Mon Sep 17 00:00:00 2001 From: Pierre Aoun Date: Mon, 5 Aug 2024 18:04:45 +0200 Subject: [PATCH 34/46] :sparkles: (keyring-eth): Create TypedData parser service --- .changeset/sixty-hotels-cheat.md | 6 + packages/core/src/api/index.ts | 2 +- packages/core/src/api/utils/HexString.test.ts | 60 -- .../core/src/api/utils/HexaString.test.ts | 130 +++++ packages/core/src/api/utils/HexaString.ts | 17 + .../keyring-eth/src/api/model/TypedData.ts | 22 +- .../internal/typed-data/di/typedDataModule.ts | 6 +- .../src/internal/typed-data/model/Types.ts | 57 ++ .../service/DefaultTypedDataParserService.ts | 16 + .../service/TypedDataEncoder.test.ts | 445 ++++++++++++++ .../typed-data/service/TypedDataEncoder.ts | 128 +++++ .../service/TypedDataParser.test.ts | 543 ++++++++++++++++++ .../typed-data/service/TypedDataParser.ts | 304 ++++++++++ .../service/TypedDataParserService.ts | 11 +- .../use-case/SignTypedDataUseCase.ts | 2 +- 15 files changed, 1679 insertions(+), 70 deletions(-) create mode 100644 .changeset/sixty-hotels-cheat.md delete mode 100644 packages/core/src/api/utils/HexString.test.ts create mode 100644 packages/core/src/api/utils/HexaString.test.ts create mode 100644 packages/signer/keyring-eth/src/internal/typed-data/model/Types.ts create mode 100644 packages/signer/keyring-eth/src/internal/typed-data/service/DefaultTypedDataParserService.ts create mode 100644 packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataEncoder.test.ts create mode 100644 packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataEncoder.ts create mode 100644 packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParser.test.ts create mode 100644 packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParser.ts diff --git a/.changeset/sixty-hotels-cheat.md b/.changeset/sixty-hotels-cheat.md new file mode 100644 index 000000000..3389a575a --- /dev/null +++ b/.changeset/sixty-hotels-cheat.md @@ -0,0 +1,6 @@ +--- +"@ledgerhq/keyring-eth": patch +"@ledgerhq/device-sdk-core": patch +--- + +DSDK-420 Implement the EIP712 TypedData parser service diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 46ffe7067..b0df795e7 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -96,4 +96,4 @@ export { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMac export { XStateDeviceAction } from "@api/device-action/xstate-utils/XStateDeviceAction"; export { type DeviceSessionState } from "@api/device-session/DeviceSessionState"; export { type SdkError } from "@api/Error"; -export { isHexaString } from "@api/utils/HexaString"; +export { hexaStringToBuffer, isHexaString } from "@api/utils/HexaString"; diff --git a/packages/core/src/api/utils/HexString.test.ts b/packages/core/src/api/utils/HexString.test.ts deleted file mode 100644 index b979fa12d..000000000 --- a/packages/core/src/api/utils/HexString.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { isHexaString } from "./HexaString"; - -describe("HexaString", () => { - describe("isHexaString function", () => { - it("should return true if the value is a valid hex string", () => { - // GIVEN - const value = "0x1234abc"; - - // WHEN - const result = isHexaString(value); - - // THEN - expect(result).toBeTruthy(); - }); - - it("should return true if no data", () => { - // GIVEN - const value = "0x"; - - // WHEN - const result = isHexaString(value); - - // THEN - expect(result).toBeTruthy(); - }); - - it("should return false if the value contain an invalid letter", () => { - // GIVEN - const value = "0x1234z"; - - // WHEN - const result = isHexaString(value); - - // THEN - expect(result).toBeFalsy(); - }); - - it("should return false if the value does not start with 0x", () => { - // GIVEN - const value = "1234abc"; - - // WHEN - const result = isHexaString(value); - - // THEN - expect(result).toBeFalsy(); - }); - - it("should return false for an epmty string", () => { - // GIVEN - const value = ""; - - // WHEN - const result = isHexaString(value); - - // THEN - expect(result).toBeFalsy(); - }); - }); -}); diff --git a/packages/core/src/api/utils/HexaString.test.ts b/packages/core/src/api/utils/HexaString.test.ts new file mode 100644 index 000000000..1606ea437 --- /dev/null +++ b/packages/core/src/api/utils/HexaString.test.ts @@ -0,0 +1,130 @@ +import { hexaStringToBuffer, isHexaString } from "./HexaString"; + +describe("HexaString", () => { + describe("isHexaString function", () => { + it("should return true if the value is a valid hex string", () => { + // GIVEN + const value = "0x1234abc"; + + // WHEN + const result = isHexaString(value); + + // THEN + expect(result).toBeTruthy(); + }); + + it("should return true if no data", () => { + // GIVEN + const value = "0x"; + + // WHEN + const result = isHexaString(value); + + // THEN + expect(result).toBeTruthy(); + }); + + it("should return false if the value contain an invalid letter", () => { + // GIVEN + const value = "0x1234z"; + + // WHEN + const result = isHexaString(value); + + // THEN + expect(result).toBeFalsy(); + }); + + it("should return false if the value does not start with 0x", () => { + // GIVEN + const value = "1234abc"; + + // WHEN + const result = isHexaString(value); + + // THEN + expect(result).toBeFalsy(); + }); + + it("should return false for an epmty string", () => { + // GIVEN + const value = ""; + + // WHEN + const result = isHexaString(value); + + // THEN + expect(result).toBeFalsy(); + }); + }); + + describe("hexaStringToBuffer function", () => { + it("should fail on empty input", () => { + // GIVEN + const value = ""; + + // WHEN + const result = hexaStringToBuffer(value); + + // THEN + expect(result).toStrictEqual(null); + }); + + it("should fail on invalid string", () => { + // GIVEN + const value = "bonjour"; + + // WHEN + const result = hexaStringToBuffer(value); + + // THEN + expect(result).toStrictEqual(null); + }); + + it("should convert correct hexadecimal string", () => { + // GIVEN + const value = "1a35669f0100"; + + // WHEN + const result = hexaStringToBuffer(value); + + // THEN + expect(result).toStrictEqual( + new Uint8Array([0x1a, 0x35, 0x66, 0x9f, 0x01, 0x00]), + ); + }); + + it("should support 0x prefix", () => { + // GIVEN + const value = "0x1a35"; + + // WHEN + const result = hexaStringToBuffer(value); + + // THEN + expect(result).toStrictEqual(new Uint8Array([0x1a, 0x35])); + }); + + it("should be case insensitive", () => { + // GIVEN + const value = "0xcCDd"; + + // WHEN + const result = hexaStringToBuffer(value); + + // THEN + expect(result).toStrictEqual(new Uint8Array([0xcc, 0xdd])); + }); + + it("should pad with 0", () => { + // GIVEN + const value = "0xa35"; + + // WHEN + const result = hexaStringToBuffer(value); + + // THEN + expect(result).toStrictEqual(new Uint8Array([0x0a, 0x35])); + }); + }); +}); diff --git a/packages/core/src/api/utils/HexaString.ts b/packages/core/src/api/utils/HexaString.ts index 1533b4194..c047fecc5 100644 --- a/packages/core/src/api/utils/HexaString.ts +++ b/packages/core/src/api/utils/HexaString.ts @@ -3,3 +3,20 @@ export type HexaString = `0x${string}`; export const isHexaString = (value: string): value is HexaString => { return /^0x[0-9a-fA-F]*$/.test(value); }; + +export const hexaStringToBuffer = (value: string): Uint8Array | null => { + if (value.length === 0) { + return null; + } + if (value.startsWith("0x")) { + value = value.slice(2); + } + if (value.length % 2 !== 0) { + value = "0" + value; + } + const bytes = value.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)); + if (!bytes || bytes.some(isNaN)) { + return null; + } + return new Uint8Array(bytes); +}; diff --git a/packages/signer/keyring-eth/src/api/model/TypedData.ts b/packages/signer/keyring-eth/src/api/model/TypedData.ts index 997c6da70..8a7deb362 100644 --- a/packages/signer/keyring-eth/src/api/model/TypedData.ts +++ b/packages/signer/keyring-eth/src/api/model/TypedData.ts @@ -1 +1,21 @@ -export type TypedData = unknown; +// As defined in https://eips.ethereum.org/EIPS/eip-712 + +export interface TypedData { + domain: TypedDataDomain; + types: Record>; + primaryType: string; + message: Record; +} + +export interface TypedDataDomain { + name?: string; + version?: string; + chainId?: number; + verifyingContract?: string; + salt?: string; +} + +export interface TypedDataField { + name: string; + type: string; +} diff --git a/packages/signer/keyring-eth/src/internal/typed-data/di/typedDataModule.ts b/packages/signer/keyring-eth/src/internal/typed-data/di/typedDataModule.ts index 3a42f0b36..4cbd92f97 100644 --- a/packages/signer/keyring-eth/src/internal/typed-data/di/typedDataModule.ts +++ b/packages/signer/keyring-eth/src/internal/typed-data/di/typedDataModule.ts @@ -1,7 +1,7 @@ import { ContainerModule } from "inversify"; import { typedDataTypes } from "@internal/typed-data/di/typedDataTypes"; -import { TypedDataParserService } from "@internal/typed-data/service/TypedDataParserService"; +import { DefaultTypedDataParserService } from "@internal/typed-data/service/DefaultTypedDataParserService"; import { SignTypedDataUseCase } from "@internal/typed-data/use-case/SignTypedDataUseCase"; export const typedDataModuleFactory = () => @@ -16,6 +16,8 @@ export const typedDataModuleFactory = () => _onDeactivation, ) => { bind(typedDataTypes.SignTypedDataUseCase).to(SignTypedDataUseCase); - bind(typedDataTypes.TypedDataParserService).to(TypedDataParserService); + bind(typedDataTypes.TypedDataParserService).to( + DefaultTypedDataParserService, + ); }, ); diff --git a/packages/signer/keyring-eth/src/internal/typed-data/model/Types.ts b/packages/signer/keyring-eth/src/internal/typed-data/model/Types.ts new file mode 100644 index 000000000..36c1a1b3a --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/typed-data/model/Types.ts @@ -0,0 +1,57 @@ +import { Maybe } from "purify-ts"; + +export type StructName = string; +export type FieldName = string; +export type FieldType = PrimitiveType | ArrayType | StructType; +export type PrimitiveTypeName = + | "int" + | "uint" + | "address" + | "bytes" + | "string" + | "bool"; + +// Basic type (address, bool, uint256, etc) +export class PrimitiveType { + constructor( + public typeName: string, + public name: PrimitiveTypeName, + public size: Maybe, + ) {} +} + +// Arrays +export class ArrayType { + constructor( + public typeName: string, + public rootType: PrimitiveType | StructType, + public rowType: string, + public count: Maybe, + public levels: Array>, + ) {} +} + +// Custom structure +export class StructType { + constructor(public typeName: string) {} +} + +// Typed data field value and metadata +export interface TypedDataValue { + path: string; + type: string; + value: TypedDataValueRoot | TypedDataValueArray | TypedDataValueField; +} + +// The value is a message root. This is usually the primaryType name. +export class TypedDataValueRoot { + constructor(public root: string) {} +} +// The value is an array. Represents the array length. +export class TypedDataValueArray { + constructor(public length: number) {} +} +// The value is a field of any type. Contains the encoded value as a byte array. +export class TypedDataValueField { + constructor(public data: Uint8Array) {} +} diff --git a/packages/signer/keyring-eth/src/internal/typed-data/service/DefaultTypedDataParserService.ts b/packages/signer/keyring-eth/src/internal/typed-data/service/DefaultTypedDataParserService.ts new file mode 100644 index 000000000..80cd928b7 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/typed-data/service/DefaultTypedDataParserService.ts @@ -0,0 +1,16 @@ +import { injectable } from "inversify"; +import { Either } from "purify-ts"; + +import { TypedData } from "@api/model/TypedData"; +import { TypedDataValue } from "@internal/typed-data/model/Types"; + +import { TypedDataParser } from "./TypedDataParser"; +import { TypedDataParserService } from "./TypedDataParserService"; + +@injectable() +export class DefaultTypedDataParserService implements TypedDataParserService { + parse(data: TypedData): Either> { + const parser = new TypedDataParser(data.types); + return parser.parse(data.primaryType, data.message); + } +} diff --git a/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataEncoder.test.ts b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataEncoder.test.ts new file mode 100644 index 000000000..adaa1a353 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataEncoder.test.ts @@ -0,0 +1,445 @@ +import { Just, Nothing } from "purify-ts"; + +import { PrimitiveType } from "@internal/typed-data/model/Types"; + +import { encodeTypedDataValue } from "./TypedDataEncoder"; + +describe("TypedDataEncoder", () => { + const ADDRESS_TYPE = new PrimitiveType("address", "address", Nothing); + const BYTES_TYPE = new PrimitiveType("bytes", "bytes", Nothing); + const BYTES_TYPE_WITH_LENGTH = new PrimitiveType("bytes", "bytes", Just(7)); + const STRING_TYPE = new PrimitiveType("string", "string", Nothing); + const BOOL_TYPE = new PrimitiveType("bool", "bool", Nothing); + const I8_TYPE = new PrimitiveType("int8", "int", Just(1)); + const I16_TYPE = new PrimitiveType("int16", "int", Just(2)); + const I32_TYPE = new PrimitiveType("int32", "int", Just(4)); + const I64_TYPE = new PrimitiveType("int64", "int", Just(8)); + const I128_TYPE = new PrimitiveType("int128", "int", Just(16)); + const I256_TYPE = new PrimitiveType("int256", "int", Just(32)); + const U8_TYPE = new PrimitiveType("uint8", "uint", Just(1)); + const U16_TYPE = new PrimitiveType("uint16", "uint", Just(2)); + const U32_TYPE = new PrimitiveType("uint32", "uint", Just(4)); + const U64_TYPE = new PrimitiveType("uint64", "uint", Just(8)); + const U128_TYPE = new PrimitiveType("uint128", "uint", Just(16)); + const U256_TYPE = new PrimitiveType("uint256", "uint", Just(32)); + + it("Encode an address", () => { + // GIVEN + const addresses = [ + "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF", + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + "0xBdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + ]; + // WHEN + const encoded = addresses.map((address) => + encodeTypedDataValue(ADDRESS_TYPE, address), + ); + // THEN + const expected = [ + Just( + Uint8Array.from([ + 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, + 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, + ]), + ), + Just( + Uint8Array.from([ + 0xcd, 0x2a, 0x3d, 0x9f, 0x93, 0x8e, 0x13, 0xcd, 0x94, 0x7e, 0xc0, + 0x5a, 0xbc, 0x7f, 0xe7, 0x34, 0xdf, 0x8d, 0xd8, 0x26, + ]), + ), + Just( + Uint8Array.from([ + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, + 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, + ]), + ), + Just( + Uint8Array.from([ + 0xb0, 0xbd, 0xab, 0xea, 0x57, 0xb0, 0xbd, 0xab, 0xea, 0x57, 0xb0, + 0xbd, 0xab, 0xea, 0x57, 0xb0, 0xbd, 0xab, 0xea, 0x57, + ]), + ), + Just( + Uint8Array.from([ + 0xbd, 0xab, 0xea, 0x57, 0xb0, 0xbd, 0xab, 0xea, 0x57, 0xb0, 0xbd, + 0xab, 0xea, 0x57, 0xb0, 0xbd, 0xab, 0xea, 0x57, + ]), + ), + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode an address with invalid size", () => { + // GIVEN + const address = "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccCff"; + // WHEN + const encoded = encodeTypedDataValue(ADDRESS_TYPE, address); + // THEN + expect(encoded).toStrictEqual(Nothing); + }); + + it("Encode an address with invalid value", () => { + // GIVEN + const address = "0xbonjourcCCCCcCCCCCCcCcCccCcCCCcCcccccccC"; + // WHEN + const encoded = encodeTypedDataValue(ADDRESS_TYPE, address); + // THEN + expect(encoded).toStrictEqual(Nothing); + }); + + it("Encode an address with invalid value type", () => { + // GIVEN + const address = 17; + // WHEN + const encoded = encodeTypedDataValue(ADDRESS_TYPE, address); + // THEN + expect(encoded).toStrictEqual(Nothing); + }); + + it("Encode an byte array", () => { + // GIVEN + const bytes = [ + "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcc", + "0x13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeFbeeF", + "0xBDabEa57", + ]; + // WHEN + const encoded = bytes.map((b) => encodeTypedDataValue(BYTES_TYPE, b)); + // THEN + const expected = [ + Just( + Uint8Array.from([ + 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, + 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, + ]), + ), + Just( + Uint8Array.from([ + 0x13, 0xcd, 0x94, 0x7e, 0xc0, 0x5a, 0xbc, 0x7f, 0xe7, 0x34, 0xdf, + 0x8d, 0xd8, 0x26, + ]), + ), + Just( + Uint8Array.from([ + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, + 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xbe, 0xef, + ]), + ), + Just(Uint8Array.from([0xbd, 0xab, 0xea, 0x57])), + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode an byte array with size", () => { + // GIVEN + const bytes = ["0x13CD947Ec05AbC", "0x13CD947Ec05A", "0x13CD947Ec05AbCde"]; + // WHEN + const encoded = bytes.map((b) => + encodeTypedDataValue(BYTES_TYPE_WITH_LENGTH, b), + ); + // THEN + const expected = [ + Just(Uint8Array.from([0x13, 0xcd, 0x94, 0x7e, 0xc0, 0x5a, 0xbc])), + Just(Uint8Array.from([0x13, 0xcd, 0x94, 0x7e, 0xc0, 0x5a])), + Nothing, + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode a string", () => { + // GIVEN + const strings = [ + "Hello, Bob!", + '"did:ethr:0xf7398bacf610bb4e3b567811279fcb3c41919f89"', + '"2021-03-04T21:08:22.615Z"^^', + "_", + ]; + // WHEN + const encoded = strings.map((str) => + encodeTypedDataValue(STRING_TYPE, str), + ); + // THEN + const expected = [ + Just( + Uint8Array.from([ + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x42, 0x6f, 0x62, 0x21, + ]), + ), + Just( + Uint8Array.from([ + 0x22, 0x64, 0x69, 0x64, 0x3a, 0x65, 0x74, 0x68, 0x72, 0x3a, 0x30, + 0x78, 0x66, 0x37, 0x33, 0x39, 0x38, 0x62, 0x61, 0x63, 0x66, 0x36, + 0x31, 0x30, 0x62, 0x62, 0x34, 0x65, 0x33, 0x62, 0x35, 0x36, 0x37, + 0x38, 0x31, 0x31, 0x32, 0x37, 0x39, 0x66, 0x63, 0x62, 0x33, 0x63, + 0x34, 0x31, 0x39, 0x31, 0x39, 0x66, 0x38, 0x39, 0x22, + ]), + ), + Just( + Uint8Array.from([ + 0x22, 0x32, 0x30, 0x32, 0x31, 0x2d, 0x30, 0x33, 0x2d, 0x30, 0x34, + 0x54, 0x32, 0x31, 0x3a, 0x30, 0x38, 0x3a, 0x32, 0x32, 0x2e, 0x36, + 0x31, 0x35, 0x5a, 0x22, 0x5e, 0x5e, 0x3c, 0x68, 0x74, 0x74, 0x70, + 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, + 0x72, 0x67, 0x2f, 0x32, 0x30, 0x30, 0x31, 0x2f, 0x58, 0x4d, 0x4c, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x23, 0x64, 0x61, 0x74, 0x65, + 0x54, 0x69, 0x6d, 0x65, 0x3e, + ]), + ), + Just( + Uint8Array.from([ + 0x5f, 0x3c, 0x64, 0x69, 0x64, 0x3a, 0x6b, 0x65, 0x79, 0x3a, 0x7a, + 0x36, 0x4d, 0x6b, 0x67, 0x46, 0x6e, 0x65, 0x61, 0x61, 0x4d, 0x6a, + 0x4e, 0x36, 0x7a, 0x79, 0x62, 0x71, 0x4c, 0x4e, 0x58, 0x67, 0x74, + 0x34, 0x59, 0x66, 0x6d, 0x56, 0x78, 0x32, 0x58, 0x5a, 0x68, 0x7a, + 0x50, 0x64, 0x44, 0x79, 0x6b, 0x34, 0x5a, 0x4b, 0x38, 0x31, 0x64, + 0x61, 0x48, 0x5a, 0x3e, + ]), + ), + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode a string with invalid value type", () => { + // GIVEN + const string = 17; + // WHEN + const encoded = encodeTypedDataValue(STRING_TYPE, string); + // THEN + expect(encoded).toStrictEqual(Nothing); + }); + + it("Encode a signed number", () => { + // GIVEN + const signed = [ + { type: I8_TYPE, value: 127 }, + { type: I8_TYPE, value: -128 }, + { type: I16_TYPE, value: "32767" }, + { type: I16_TYPE, value: "-32768" }, + { type: I32_TYPE, value: "0x7FFFFFFF" }, + { type: I32_TYPE, value: "-2147483648" }, + { type: I64_TYPE, value: 9223372036854775807n }, + { type: I64_TYPE, value: -9223372036854775808n }, + { type: I128_TYPE, value: "170141183460469231731687303715884105727" }, + { type: I128_TYPE, value: "-170141183460469231731687303715884105728" }, + { + type: I256_TYPE, + value: + 57896044618658097711785492504343953926634992332820282019728792003956564819967n, + }, + { + type: I256_TYPE, + value: + -57896044618658097711785492504343953926634992332820282019728792003956564819968n, + }, + ]; + // WHEN + const encoded = signed.map((s) => encodeTypedDataValue(s.type, s.value)); + // THEN + const expected = [ + Just(Uint8Array.from([0x7f])), + Just(Uint8Array.from([0x80])), + Just(Uint8Array.from([0x7f, 0xff])), + Just(Uint8Array.from([0x80, 0x00])), + Just(Uint8Array.from([0x7f, 0xff, 0xff, 0xff])), + Just(Uint8Array.from([0x80, 0x00, 0x00, 0x00])), + Just(Uint8Array.from([0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])), + Just(Uint8Array.from([0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])), + Just( + Uint8Array.from([ + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, + ]), + ), + Just( + Uint8Array.from([ + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + ), + Just( + Uint8Array.from([ + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]), + ), + Just( + Uint8Array.from([ + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + ), + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode a signed number, out of bounds", () => { + // GIVEN + const signed = [ + { type: I8_TYPE, value: "" }, + { type: I8_TYPE, value: 128 }, + { type: I8_TYPE, value: -129 }, + { type: I16_TYPE, value: "32768" }, + { type: I16_TYPE, value: "-32769" }, + { type: I32_TYPE, value: "0xFFFFFFFF" }, + { type: I32_TYPE, value: "-2147483649" }, + { type: I64_TYPE, value: 9223372036854775808n }, + { type: I64_TYPE, value: -9223372036854775809n }, + { type: I128_TYPE, value: "170141183460469231731687303715884105728" }, + { type: I128_TYPE, value: "-170141183460469231731687303715884105729" }, + { + type: I256_TYPE, + value: + 57896044618658097711785492504343953926634992332820282019728792003956564819968n, + }, + { + type: I256_TYPE, + value: + -57896044618658097711785492504343953926634992332820282019728792003956564819969n, + }, + ]; + // WHEN + const encoded = signed.map((s) => encodeTypedDataValue(s.type, s.value)); + // THEN + const expected = [ + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode an unsigned number", () => { + // GIVEN + const unsigned = [ + { type: U8_TYPE, value: 0 }, + { type: U8_TYPE, value: 255 }, + { type: U16_TYPE, value: "65535" }, + { type: U32_TYPE, value: "0xFFFFFFFF" }, + { type: U64_TYPE, value: 18446744073709551615n }, + { type: U128_TYPE, value: "340282366920938463463374607431768211455" }, + { + type: U256_TYPE, + value: + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + }, + ]; + // WHEN + const encoded = unsigned.map((s) => encodeTypedDataValue(s.type, s.value)); + // THEN + const expected = [ + Just(Uint8Array.from([0x00])), + Just(Uint8Array.from([0xff])), + Just(Uint8Array.from([0xff, 0xff])), + Just(Uint8Array.from([0xff, 0xff, 0xff, 0xff])), + Just(Uint8Array.from([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])), + Just( + Uint8Array.from([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, + ]), + ), + Just( + Uint8Array.from([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]), + ), + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode an unsigned number, out of bound", () => { + // GIVEN + const unsigned = [ + { type: U8_TYPE, value: "" }, + { type: U8_TYPE, value: 0.5 }, + { type: U8_TYPE, value: -1 }, + { type: U8_TYPE, value: 256 }, + { type: U16_TYPE, value: "65536" }, + { type: U32_TYPE, value: "0x100000000" }, + { type: U64_TYPE, value: 18446744073709551616n }, + { type: U128_TYPE, value: "340282366920938463463374607431768211456" }, + { + type: U256_TYPE, + value: + 115792089237316195423570985008687907853269984665640564039457584007913129639936n, + }, + ]; + // WHEN + const encoded = unsigned.map((s) => encodeTypedDataValue(s.type, s.value)); + // THEN + const expected = [ + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + Nothing, + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode a boolean", () => { + // GIVEN + const bools = [false, true, 0, 1, "0", "0x1"]; + // WHEN + const encoded = bools.map((b) => encodeTypedDataValue(BOOL_TYPE, b)); + // THEN + const expected = [ + Just(Uint8Array.from([0x00])), + Just(Uint8Array.from([0x01])), + Just(Uint8Array.from([0x00])), + Just(Uint8Array.from([0x01])), + Just(Uint8Array.from([0x00])), + Just(Uint8Array.from([0x01])), + ]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode a boolean, out of bounds", () => { + // GIVEN + const bools = [-1, 2]; + // WHEN + const encoded = bools.map((b) => encodeTypedDataValue(BOOL_TYPE, b)); + // THEN + const expected = [Nothing, Nothing]; + expect(encoded).toStrictEqual(expected); + }); + + it("Encode an invalid data type", () => { + // GIVEN + const data = [ + { type: U8_TYPE, value: true }, + { type: I8_TYPE, value: {} }, + { type: BOOL_TYPE, value: undefined }, + { type: STRING_TYPE, value: 42 }, + { type: BYTES_TYPE, value: 42 }, + { type: ADDRESS_TYPE, value: false }, + ]; + // WHEN + const encoded = data.map((d) => encodeTypedDataValue(d.type, d.value)); + // THEN + const expected = [Nothing, Nothing, Nothing, Nothing, Nothing, Nothing]; + expect(encoded).toStrictEqual(expected); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataEncoder.ts b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataEncoder.ts new file mode 100644 index 000000000..4b70570ba --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataEncoder.ts @@ -0,0 +1,128 @@ +import { hexaStringToBuffer } from "@ledgerhq/device-sdk-core"; +import { Just, Maybe, Nothing } from "purify-ts"; + +import { + PrimitiveType, + PrimitiveTypeName, +} from "@internal/typed-data/model/Types"; + +/** + * Encodes a typed data value according to its type. + * @param type The type of the value to encode. + * @param value The value to encode into a byte array. + * @returns An optional Uint8Array containing the encoded value, if the value was encodable for the given type. + */ +export function encodeTypedDataValue( + type: PrimitiveType, + value: unknown, +): Maybe { + switch (type.name) { + case "string": + // Encode the value as a UTF-8 string + return typeof value !== "string" + ? Nothing + : Just(new TextEncoder().encode(value)); + case "bytes": + case "address": + // Encode the hexadecimal string as bytes + return typeof value !== "string" + ? Nothing + : encodeTypedDataBytes(type, value); + case "bool": + case "uint": + case "int": + // Convert boolean values to numbers, so it can then be encoded as a number + if (type.name === "bool" && typeof value === "boolean") { + value = Number(value); + } + // Encode the value as a number + return typeof value !== "string" && + typeof value !== "number" && + typeof value !== "bigint" + ? Nothing + : encodeTypedDataNumber( + type.name, + type.size.mapOrDefault((s) => s * 8, 1), // Size in bits + value, + ); + } +} + +function encodeTypedDataBytes( + type: PrimitiveType, + value: string, +): Maybe { + const maxSize = type.name === "address" ? Just(20) : type.size; + const buffer = Maybe.fromNullable(hexaStringToBuffer(value)); + return buffer.filter((b) => maxSize.mapOrDefault((s) => b.length <= s, true)); +} + +function encodeTypedDataNumber( + type: PrimitiveTypeName, + sizeInBits: number, + value: string | number | bigint, +): Maybe { + // Convert the value to a bigint + let bigintValue: bigint; + switch (typeof value) { + case "bigint": + bigintValue = value; + break; + case "number": + if (!Number.isInteger(value)) { + return Nothing; + } + bigintValue = BigInt(value); + break; + case "string": + if (value.length === 0) { + return Nothing; + } + try { + bigintValue = BigInt(value); + } catch (e: unknown) { + return Nothing; + } + break; + } + // Check the bounds of the value and convert it to two's complement if it is signed and negative + const signed = type === "int"; + return checkBoundsAndConvert(bigintValue, BigInt(sizeInBits), signed).chain( + (converted) => + Maybe.fromNullable(hexaStringToBuffer(converted.toString(16))), + ); +} + +/** + * Checks the bounds of a signed or unsigned integer value and converts it to two's complement if it is signed and negative. + * @param value The value to check and convert. + * @param sizeInBits The size of the value in bits. + * @param signed Whether the value is signed or unsigned. + * @returns The converted value, or null if the value is out of bounds. + */ +function checkBoundsAndConvert( + value: bigint, + sizeInBits: bigint, + signed: boolean, +): Maybe { + if (!signed) { + // Check if the value is within the bounds of an unsigned integer + return value >= 0n && value < 1n << sizeInBits ? Just(value) : Nothing; + } + + // Check if the value is within the bounds of a signed integer + const limit = 1n << (sizeInBits - 1n); + if (value >= limit || value < -limit) { + return Nothing; + } + + // Convert the value to two's complement if it is negative + // https://en.wikipedia.org/wiki/Two%27s_complement + if (value < 0n) { + const mask = (1n << sizeInBits) - 1n; + value = -value; + value = (~value & mask) + 1n; + } + + return Just(value); +} diff --git a/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParser.test.ts b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParser.test.ts new file mode 100644 index 000000000..c170c7e47 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParser.test.ts @@ -0,0 +1,543 @@ +import { hexaStringToBuffer } from "@ledgerhq/device-sdk-core"; +import { Just, Nothing, Right } from "purify-ts"; + +import { TypedData } from "@api/model/TypedData"; +import { + ArrayType, + PrimitiveType, + StructType, + TypedDataValueArray, + TypedDataValueField, + TypedDataValueRoot, +} from "@internal/typed-data/model/Types"; + +import { TypedDataParser } from "./TypedDataParser"; + +describe("TypedDataParser - types parsing", () => { + it("Parse primitive types bytes", () => { + // GIVEN + const types = { + TestStruct: [ + { name: "test1", type: "bytes" }, + { name: "test2", type: "bytes1" }, + { name: "test3", type: "bytes2" }, + { name: "test4", type: "bytes31" }, + { name: "test5", type: "bytes32" }, + ], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const expected = { + TestStruct: { + test1: new PrimitiveType("bytes", "bytes", Nothing), + test2: new PrimitiveType("bytes1", "bytes", Just(1)), + test3: new PrimitiveType("bytes2", "bytes", Just(2)), + test4: new PrimitiveType("bytes31", "bytes", Just(31)), + test5: new PrimitiveType("bytes32", "bytes", Just(32)), + }, + }; + expect(parser.getStructDefinitions()).toStrictEqual(expected); + }); + + it("Parse primitive types bytes, out of bound", () => { + // GIVEN + const types = { + TestStruct: [ + { name: "invalid1", type: "bytes0" }, + { name: "invalid2", type: "bytes33" }, + ], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const expected = { + TestStruct: { + invalid1: new StructType("bytes0"), + invalid2: new StructType("bytes33"), + }, + }; + expect(parser.getStructDefinitions()).toStrictEqual(expected); + }); + + it("Parse primitive types number", () => { + // GIVEN + const types = { + TestStruct: [ + { name: "test1", type: "int8" }, + { name: "test2", type: "uint8" }, + { name: "test3", type: "int16" }, + { name: "test4", type: "uint32" }, + { name: "test5", type: "uint64" }, + { name: "test6", type: "int128" }, + { name: "test7", type: "int136" }, + { name: "test8", type: "int144" }, + { name: "test9", type: "uint240" }, + { name: "test10", type: "uint248" }, + { name: "test11", type: "uint256" }, + { name: "test12", type: "int256" }, + ], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const expected = { + TestStruct: { + test1: new PrimitiveType("int8", "int", Just(1)), + test2: new PrimitiveType("uint8", "uint", Just(1)), + test3: new PrimitiveType("int16", "int", Just(2)), + test4: new PrimitiveType("uint32", "uint", Just(4)), + test5: new PrimitiveType("uint64", "uint", Just(8)), + test6: new PrimitiveType("int128", "int", Just(16)), + test7: new PrimitiveType("int136", "int", Just(17)), + test8: new PrimitiveType("int144", "int", Just(18)), + test9: new PrimitiveType("uint240", "uint", Just(30)), + test10: new PrimitiveType("uint248", "uint", Just(31)), + test11: new PrimitiveType("uint256", "uint", Just(32)), + test12: new PrimitiveType("int256", "int", Just(32)), + }, + }; + expect(parser.getStructDefinitions()).toStrictEqual(expected); + }); + + it("Parse primitive types number, out of bound", () => { + // GIVEN + const types = { + TestStruct: [ + { name: "invalid1", type: "int0" }, + { name: "invalid2", type: "uint0" }, + { name: "invalid3", type: "int7" }, + { name: "invalid4", type: "int257" }, + { name: "invalid5", type: "uint257" }, + { name: "invalid6", type: "int512" }, + ], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const expected = { + TestStruct: { + invalid1: new StructType("int0"), + invalid2: new StructType("uint0"), + invalid3: new StructType("int7"), + invalid4: new StructType("int257"), + invalid5: new StructType("uint257"), + invalid6: new StructType("int512"), + }, + }; + expect(parser.getStructDefinitions()).toStrictEqual(expected); + }); + + it("Parse primitive types others", () => { + // GIVEN + const types = { + TestStruct: [ + { name: "test1", type: "address" }, + { name: "test2", type: "bool" }, + { name: "test3", type: "string" }, + ], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const expected = { + TestStruct: { + test1: new PrimitiveType("address", "address", Nothing), + test2: new PrimitiveType("bool", "bool", Nothing), + test3: new PrimitiveType("string", "string", Nothing), + }, + }; + expect(parser.getStructDefinitions()).toStrictEqual(expected); + }); + + it("Parse arrays", () => { + // GIVEN + const types = { + TestStruct: [ + { name: "test1", type: "address[]" }, + { name: "test2", type: "uint16[3]" }, + { name: "test3", type: "custom[2][][3]" }, + { name: "test4", type: "string[2][][3][]" }, + ], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const expected = { + TestStruct: { + test1: new ArrayType( + "address[]", + new PrimitiveType("address", "address", Nothing), + "address", + Nothing, + [Nothing], + ), + test2: new ArrayType( + "uint16[3]", + new PrimitiveType("uint16", "uint", Just(2)), + "uint16", + Just(3), + [Just(3)], + ), + test3: new ArrayType( + "custom[2][][3]", + new StructType("custom"), + "custom[2][]", + Just(3), + [Just(2), Nothing, Just(3)], + ), + test4: new ArrayType( + "string[2][][3][]", + new PrimitiveType("string", "string", Nothing), + "string[2][][3]", + Nothing, + [Just(2), Nothing, Just(3), Nothing], + ), + }, + }; + expect(parser.getStructDefinitions()).toStrictEqual(expected); + }); + + it("Parse custom struct", () => { + // GIVEN + const types = { + TestStruct: [{ name: "test", type: "MyCustomStructure" }], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const expected = { + TestStruct: { + test: new StructType("MyCustomStructure"), + }, + }; + expect(parser.getStructDefinitions()).toStrictEqual(expected); + }); +}); + +describe("TypedDataParser - message parsing", () => { + const MESSAGE: TypedData = { + domain: { + chainId: 5, + name: "Ether Mail", + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + version: "1", + }, + message: { + contents: "Hello, Bob!", + from: { + name: "Cow", + wallets: [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF", + ], + }, + to: [ + { + wallets: [ + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + "0xB0B0b0b0b0b0B000000000000000000000000000", + ], + name: "Bob", + }, + ], + }, + primaryType: "Mail", + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person[1]" }, + { name: "contents", type: "string" }, + ], + Person: [ + { name: "name", type: "string" }, + { name: "wallets", type: "address[]" }, + ], + }, + }; + + it("Parse an EIP712 message", () => { + // GIVEN + const types = MESSAGE.types; + const primaryType = MESSAGE.primaryType; + const message = MESSAGE.message; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + const expected = [ + { + path: "", + type: "", + value: new TypedDataValueRoot(primaryType), + }, + { + path: "from.name", + type: "string", + value: new TypedDataValueField(new TextEncoder().encode("Cow")), + }, + { + path: "from.wallets", + type: "address[]", + value: new TypedDataValueArray(2), + }, + { + path: "from.wallets.[]", + type: "address", + value: new TypedDataValueField( + hexaStringToBuffer("0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826")!, + ), + }, + { + path: "from.wallets.[]", + type: "address", + value: new TypedDataValueField( + hexaStringToBuffer("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF")!, + ), + }, + { path: "to", type: "Person[1]", value: new TypedDataValueArray(1) }, + { + path: "to.[].name", + type: "string", + value: new TypedDataValueField(new TextEncoder().encode("Bob")), + }, + { + path: "to.[].wallets", + type: "address[]", + value: new TypedDataValueArray(3), + }, + { + path: "to.[].wallets.[]", + type: "address", + value: new TypedDataValueField( + hexaStringToBuffer("0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB")!, + ), + }, + { + path: "to.[].wallets.[]", + type: "address", + value: new TypedDataValueField( + hexaStringToBuffer("0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57")!, + ), + }, + { + path: "to.[].wallets.[]", + type: "address", + value: new TypedDataValueField( + hexaStringToBuffer("0xB0B0b0b0b0b0B000000000000000000000000000")!, + ), + }, + { + path: "contents", + type: "string", + value: new TypedDataValueField(new TextEncoder().encode("Hello, Bob!")), + }, + ]; + expect(parsed).toStrictEqual(Right(expected)); + }); + + it("Parse an EIP712 domain", () => { + // GIVEN + const types = MESSAGE.types; + const primaryType = "EIP712Domain"; + const message = MESSAGE.domain; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const expected = [ + { + path: "", + type: "", + value: new TypedDataValueRoot(primaryType), + }, + { + path: "name", + type: "string", + value: new TypedDataValueField(new TextEncoder().encode("Ether Mail")), + }, + { + path: "version", + type: "string", + value: new TypedDataValueField(new TextEncoder().encode("1")), + }, + { + path: "chainId", + type: "uint256", + value: new TypedDataValueField(Uint8Array.from([5])), + }, + { + path: "verifyingContract", + type: "address", + value: new TypedDataValueField( + hexaStringToBuffer("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC")!, + ), + }, + ]; + const parsed = parser.parse(primaryType, message); + expect(parsed).toStrictEqual(Right(expected)); + }); + + it("Invalid primary type", () => { + // GIVEN + const types = MESSAGE.types; + const primaryType = "unknown"; + const message = MESSAGE.domain; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Struct points to an unknown custom type", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "Person" }], + }; + const primaryType = "Mail"; + const message = { + from: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Array contains an unknown custom type", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "Person[]" }], + }; + const primaryType = "Mail"; + const message = { + from: ["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Invalid primitive value", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "uint8" }], + }; + const primaryType = "Mail"; + const message = { + from: 3000, + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Array instead of primitive value", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "uint8" }], + }; + const primaryType = "Mail"; + const message = { + from: [42], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Struct instead of primitive value", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "uint8" }], + }; + const primaryType = "Mail"; + const message = { + from: { data: 42 }, + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Struct value not a record", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "Person" }], + Person: [{ name: "data", type: "uint8" }], + }; + const primaryType = "Mail"; + const message = { + from: 42, + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Struct field not present in value", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "uint8" }], + }; + const primaryType = "Mail"; + const message = { + to: 42, + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Array value not an array", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "uint8[]" }], + }; + const primaryType = "Mail"; + const message = { + from: 42, + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); + + it("Array value with invalid size", () => { + // GIVEN + const types = { + Mail: [{ name: "from", type: "uint8[3]" }], + }; + const primaryType = "Mail"; + const message = { + from: [42], + }; + // WHEN + const parser = new TypedDataParser(types); + // THEN + const parsed = parser.parse(primaryType, message); + expect(parsed.isLeft()).toStrictEqual(true); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParser.ts b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParser.ts new file mode 100644 index 000000000..a484fcbe0 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParser.ts @@ -0,0 +1,304 @@ +import { Either, Just, Left, Maybe, Nothing, Right } from "purify-ts"; + +import { TypedDataField } from "@api/model/TypedData"; +import { + ArrayType, + FieldName, + FieldType, + PrimitiveType, + StructName, + StructType, + TypedDataValue, + TypedDataValueArray, + TypedDataValueField, + TypedDataValueRoot, +} from "@internal/typed-data/model/Types"; + +import { encodeTypedDataValue } from "./TypedDataEncoder"; + +/** + * A parser for EIP-712 typed data messages. + * + * ```typescript + * const types = { + * Person: [ + * { name: 'name', type: 'string' }, + * { name: 'age', type: 'uint256' }, + * { name: "wallets", type: "address[]" }, + * ], + * }; + * const parser = new TypedDataParser(types); + * + * const message = { + * name: 'Alice', + * age: 30, + * wallets: [ + * "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + * "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + * ], + * }; + * const rootType = 'Person'; + * + * const result = parser.parse(rootType, message); + * ``` + */ +export class TypedDataParser { + private readonly structs: Record>; + + /** + * Creates a new instance of the TypedDataParser class. + * @param types The types to be used for parsing the message. + */ + constructor(types: Record>) { + // Parse the types to be used later for parsing a message. + const structs: Record> = {}; + for (const [typedName, typedData] of Object.entries(types)) { + const parsedTypedData: Record = {}; + for (const data of typedData) { + parsedTypedData[data.name] = this.parseType(data.type); + } + structs[typedName] = parsedTypedData; + } + this.structs = structs; + } + + /** + * Returns the parsed definitions of custom structs as defined in the types passed to the constructor. + * @returns The struct definitions. + */ + public getStructDefinitions(): Record< + StructName, + Record + > { + return this.structs; + } + + /** + * Parses a message according to the primary type and the types passed to the constructor. + * @param primaryType The root type of the message. + * @param message The message to parse. + * @returns An Either containing the parsed values or an error. + */ + public parse( + primaryType: string, + message: unknown, + ): Either> { + if (!this.isRecord(message)) { + return Left(new Error("Message is not a record")); + } + const values: Array = [ + { + path: "", + type: "", + value: new TypedDataValueRoot(primaryType), + }, + ]; + return this.visitValue(primaryType, message, "", (val) => values.push(val)) + ? Right(values) + : Left(new Error("Failed to parse")); + } + + private isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; + } + + /** + * Parses a type string description into a PrimitiveType, ArrayType, or StructType object. + * The description string should match https://eips.ethereum.org/EIPS/eip-712#definition-of-typed-structured-data-%F0%9D%95%8A + * Any string which doesn't match those patterns is considered a custom struct. + * @param type The type string to parse. + * @returns The parsed type object. + */ + private parseType(type: string): PrimitiveType | ArrayType | StructType { + return this.tryParsePrimitiveType(type).mapOrDefault( + (just) => just, + this.tryParseArrayType(type).mapOrDefault( + (just) => just, + new StructType(type), + ), + ); + } + + private tryParsePrimitiveType(type: string): Maybe { + // int8 to int256 and uint8 to uint256 + { + const match = type.match(/^(((u?)int)(\d+))$/); + if (match) { + const size = parseInt(match[4]!); + if (size % 8 !== 0 || size === 0 || size > 256) { + return Nothing; // Unsupported number + } + return Just( + new PrimitiveType( + match[1]!, // typeName such as uint64 + match[3] ? "uint" : "int", // name such as uint + Just(size / 8), // size in bytes such as 8 for an uint64 + ), + ); + } + } + + // bytes1 to bytes32, or bytes (dynamic size) + { + const match = type.match(/^((bytes)(\d*))$/); + if (match) { + const size = match[3] ? parseInt(match[3]) : null; + if (size !== null && (size === 0 || size > 32)) { + return Nothing; // Unsupported byte array + } + return Just( + new PrimitiveType( + match[1]!, // typename such as bytes32 + "bytes", // name + Maybe.fromNullable(size), // size in bytes, or null for a dynamic size + ), + ); + } + } + + // Other primitive types + if (type === "address" || type === "bool" || type === "string") { + return Just( + new PrimitiveType( + type, // typeName + type, // name + Nothing, // size not applicable for those types + ), + ); + } + + // Not a primitive type + return Nothing; + } + + private tryParseArrayType(type: string): Maybe { + // Try to match an array such as: foo[2][][3] + const match = type.match(/^([^[[]*)(((\[\d*\])*)\[\d*\])$/); + if (match) { + const matchLevels = [...match[2]!.matchAll(/\[(\d*)\]/g)]; + if (matchLevels && matchLevels.length > 0) { + const levels = matchLevels.map(([, size]) => + size ? Just(parseInt(size)) : Nothing, + ); + const rootType = this.tryParsePrimitiveType(match[1]!).mapOrDefault( + (just) => just, + new StructType(match[1]!), + ); + return Just( + new ArrayType( + type, // typeName such as: foo[2][][3] + rootType, // rootType such as: foo + match[1]! + match[3], // rowType such as: foo[2][] + levels[levels.length - 1]!, // rows count such as: 3 + levels, // All levels for that array (null for dynamic size), such as: [2, null, 3] + ), + ); + } + } + + // Not an array + return Nothing; + } + + /** + * Visits a value and its children recursively, parsing them into TypedDataValue objects. + * @param type The type of the value. + * @param value The value to visit. + * @param path The path of the value. + * @param callback The callback to call for each parsed value. + * @returns True if the value and its children were successfully parsed, false otherwise. + */ + private visitValue( + type: string, + value: unknown, + path: string, + callback: (parsedValue: TypedDataValue) => void, + ): boolean { + return ( + this.tryVisitStructValue(type, value, path, callback) || + this.tryVisitPrimitiveValue(type, value, path, callback) || + this.tryVisitArrayValue(type, value, path, callback) + ); + } + + private tryVisitPrimitiveValue( + type: string, + value: unknown, + path: string, + callback: (parsedValue: TypedDataValue) => void, + ): boolean { + // Basic type (address, bool, uint256, etc) + return ( + !this.isRecord(value) && + !Array.isArray(value) && + this.tryParsePrimitiveType(type) + .chain((primitiveType) => + encodeTypedDataValue(primitiveType, value).ifJust((encoded) => { + callback({ + path, + type, + value: new TypedDataValueField(encoded), + }); + }), + ) + .isJust() + ); + } + + private tryVisitStructValue( + type: string, + value: unknown, + path: string, + callback: (parsedValue: TypedDataValue) => void, + ): boolean { + const structType = this.structs[type]; + if (!structType || !this.isRecord(value)) { + return false; + } + for (const [fieldName, fieldType] of Object.entries(structType)) { + const fieldValue = value[fieldName]; + if (!fieldType || !fieldValue) { + return false; + } + const nextPath = path.length ? `${path}.${fieldName}` : fieldName; + if ( + !this.visitValue( + fieldType.typeName, + fieldValue, + `${nextPath}`, + callback, + ) + ) { + return false; + } + } + return true; + } + + private tryVisitArrayValue( + type: string, + value: unknown, + path: string, + callback: (parsedValue: TypedDataValue) => void, + ): boolean { + return ( + Array.isArray(value) && + this.tryParseArrayType(type) + .filter((t) => t.count.mapOrDefault((c) => value.length == c, true)) + .mapOrDefault((t) => { + callback({ + path: path, + type, + value: new TypedDataValueArray(value.length), + }); + for (const entry of value) { + const nextPath = path.length ? `${path}.[]` : "[]"; + if (!this.visitValue(t.rowType, entry, `${nextPath}`, callback)) { + return false; + } + } + return true; + }, false) + ); + } +} diff --git a/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParserService.ts b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParserService.ts index 1ae5978a8..1e96de124 100644 --- a/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParserService.ts +++ b/packages/signer/keyring-eth/src/internal/typed-data/service/TypedDataParserService.ts @@ -1,7 +1,8 @@ -import { injectable } from "inversify"; +import { Either } from "purify-ts"; -@injectable() -export class TypedDataParserService { - // Parse a typed data object into APDU commands - constructor() {} +import { TypedData } from "@api/model/TypedData"; +import { TypedDataValue } from "@internal/typed-data/model/Types"; + +export interface TypedDataParserService { + parse(message: TypedData): Either>; } diff --git a/packages/signer/keyring-eth/src/internal/typed-data/use-case/SignTypedDataUseCase.ts b/packages/signer/keyring-eth/src/internal/typed-data/use-case/SignTypedDataUseCase.ts index da851e1c7..dd06b35a2 100644 --- a/packages/signer/keyring-eth/src/internal/typed-data/use-case/SignTypedDataUseCase.ts +++ b/packages/signer/keyring-eth/src/internal/typed-data/use-case/SignTypedDataUseCase.ts @@ -5,7 +5,7 @@ import { TypedData } from "@api/model/TypedData"; import { appBinderTypes } from "@internal/app-binder/di/appBinderTypes"; import { EthAppBinder } from "@internal/app-binder/EthAppBinder"; import { typedDataTypes } from "@internal/typed-data/di/typedDataTypes"; -import { TypedDataParserService } from "@internal/typed-data/service/TypedDataParserService"; +import { type TypedDataParserService } from "@internal/typed-data/service/TypedDataParserService"; @injectable() export class SignTypedDataUseCase { From 99c4045d198e99de9a72536bf0f2c0101e88117e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 11 Aug 2024 13:07:13 +0000 Subject: [PATCH 35/46] :arrow_up: (repo) [NO-ISSUE]: Bump zx from 8.1.2 to 8.1.4 Bumps [zx](https://github.com/google/zx) from 8.1.2 to 8.1.4. - [Release notes](https://github.com/google/zx/releases) - [Commits](https://github.com/google/zx/compare/8.1.2...8.1.4) --- updated-dependencies: - dependency-name: zx dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e5b4fd58d..c1508b3ce 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "tsc-alias": "^1.8.10", "turbo": "^2.0.9", "typescript": "^5.5.3", - "zx": "^8.1.2" + "zx": "^8.1.4" }, "engines": { "node": ">=18" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c32f60b6..4c8359e15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: ^5.5.3 version: 5.5.3 zx: - specifier: ^8.1.2 - version: 8.1.2 + specifier: ^8.1.4 + version: 8.1.4 apps/sample: dependencies: @@ -6405,8 +6405,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zx@8.1.2: - resolution: {integrity: sha512-zkCiXKh8D/eo6r58OmJvO5mc2NthcSRvysb3fuS6VQlHPbEPBcxduRwM3m6ZfHj+7cLHcrahCnuO2TDAbW+6xw==} + zx@8.1.4: + resolution: {integrity: sha512-QFDYYpnzdpRiJ3dL2102Cw26FpXpWshW4QLTGxiYfIcwdAqg084jRCkK/kuP/NOSkxOjydRwNFG81qzA5r1a6w==} engines: {node: '>= 12.17.0'} hasBin: true @@ -14074,7 +14074,7 @@ snapshots: yocto-queue@0.1.0: {} - zx@8.1.2: + zx@8.1.4: optionalDependencies: '@types/fs-extra': 11.0.4 '@types/node': 20.14.11 From 48a348ae1a275722303f2fc9380863fff25d375f Mon Sep 17 00:00:00 2001 From: Pierre Aoun Date: Tue, 13 Aug 2024 10:44:40 +0200 Subject: [PATCH 36/46] :sparkles: (keyring-eth): Implement SendEIP712StructDefinition command --- .changeset/thick-zoos-travel.md | 5 + .../SendEIP712StructDefinitionCommand.test.ts | 374 ++++++++++++++++++ .../SendEIP712StructDefinitionCommand.ts | 168 +++++++- 3 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 .changeset/thick-zoos-travel.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructDefinitionCommand.test.ts diff --git a/.changeset/thick-zoos-travel.md b/.changeset/thick-zoos-travel.md new file mode 100644 index 000000000..4faf47363 --- /dev/null +++ b/.changeset/thick-zoos-travel.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement SendEIP712StructDefinitionCommand diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructDefinitionCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructDefinitionCommand.test.ts new file mode 100644 index 000000000..95b48fbfe --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructDefinitionCommand.test.ts @@ -0,0 +1,374 @@ +import { Command, InvalidStatusWordError } from "@ledgerhq/device-sdk-core"; +import { Just, Nothing } from "purify-ts"; + +import { + ArrayType, + PrimitiveType, + StructType, +} from "@internal/typed-data/model/Types"; + +import { + SendEIP712StructDefinitionCommand, + SendEIP712StructDefinitionCommandArgs, + StructDefinitionCommand, +} from "./SendEIP712StructDefinitionCommand"; + +const EIP712_DEF_NAME_EIP712DOMAIN = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0x00, 0x0c, 0x45, 0x49, 0x50, 0x37, 0x31, 0x32, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, +]); + +const EIP712_DEF_NAME_GROUP = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0x00, 0x05, 0x47, 0x72, 0x6f, 0x75, 0x70, +]); + +// name string +const EIP712_DEF_FIELD_NAME_STRING = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x06, 0x05, 0x04, 0x6e, 0x61, 0x6d, 0x65, +]); + +// version string +const EIP712_DEF_FIELD_VERSION_STRING = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x09, 0x05, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, +]); + +// chainId uint256 +const EIP712_DEF_FIELD_CHAINID_UINT256 = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x0a, 0x42, 0x20, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x49, 0x64, +]); + +// verifyingContract address +const EIP712_DEF_FIELD_VERIFYINGCONTRACT_ADDRESS = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x13, 0x03, 0x11, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, + 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, +]); + +// members Person[] +const EIP712_DEF_FIELD_MEMBERS_PERSON = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x12, 0x80, 0x06, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, + 0x01, 0x00, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, +]); + +// from Person +const EIP712_DEF_FIELD_FROM_PERSON = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x0d, 0x00, 0x06, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, + 0x04, 0x66, 0x72, 0x6f, 0x6d, +]); + +// wallets address[] +const EIP712_DEF_FIELD_WALLETS_ADDRESS = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x0b, 0x83, 0x01, 0x00, 0x07, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x73, +]); + +// staticExtradata bytes +const EIP712_DEF_FIELD_STATICEXTRADATA_BYTES = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x11, 0x07, 0x0f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, + 0x45, 0x78, 0x74, 0x72, 0x61, 0x64, 0x61, 0x74, 0x61, +]); + +// replacementPattern bytes +const EIP712_DEF_FIELD_REPLACEMENTPATTERN_BYTES = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x14, 0x07, 0x12, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, +]); + +// dataType bytes4 +const EIP712_DEF_FIELD_DATA_TYPE_BYTES4 = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x0b, 0x46, 0x04, 0x08, 0x64, 0x61, 0x74, 0x61, 0x54, + 0x79, 0x70, 0x65, +]); + +// document string[3][] +const EIP712_DEF_FIELD_DOCUMENT_STRING = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x0e, 0x85, 0x02, 0x01, 0x03, 0x00, 0x08, 0x64, 0x6f, + 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, +]); + +// depthy uint8[][][][] +const EIP712_DEF_FIELD_DEPTHY_UINT8 = Uint8Array.from([ + 0xe0, 0x1a, 0x00, 0xff, 0x0e, 0xc2, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x06, + 0x64, 0x65, 0x70, 0x74, 0x68, 0x79, +]); + +// TODO: find examples for bool and int types. + +describe("SendEIP712StructDefinitionCommand", () => { + let command: Command; + + describe("getApdu", () => { + it("should return the apdu for 'EIP712Domain' name definition", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Name, + name: "EIP712Domain", + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_NAME_EIP712DOMAIN); + }); + + it("should return the apdu for 'Group' name definition", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Name, + name: "Group", + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_NAME_GROUP); + }); + + it("should return the apdu for 'name' of type 'string'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "name", + type: new PrimitiveType("string", "string", Nothing), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_FIELD_NAME_STRING); + }); + + it("should return the apdu for 'version' of type 'string'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "version", + type: new PrimitiveType("string", "string", Nothing), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_FIELD_VERSION_STRING); + }); + + it("should return the apdu for 'chainId' of type 'uint256'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "chainId", + type: new PrimitiveType("uint256", "uint", Just(32)), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_FIELD_CHAINID_UINT256); + }); + + it("should return the apdu for 'verifyingContract' of type 'address'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "verifyingContract", + type: new PrimitiveType("address", "address", Nothing), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + EIP712_DEF_FIELD_VERIFYINGCONTRACT_ADDRESS, + ); + }); + + it("should return the apdu for 'members' of type 'Person[]'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "members", + type: new ArrayType( + "Person[]", + new StructType("Person"), + "Person", + Nothing, + [Nothing], + ), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_FIELD_MEMBERS_PERSON); + }); + + it("should return the apdu for 'from' of type 'Person'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "from", + type: new StructType("Person"), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_FIELD_FROM_PERSON); + }); + + it("should return the apdu for 'wallets' of type 'address[]'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "wallets", + type: new ArrayType( + "address[]", + new PrimitiveType("address", "address", Nothing), + "address", + Nothing, + [Nothing], + ), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_FIELD_WALLETS_ADDRESS); + }); + + it("should return the apdu for 'staticExtradata' of type 'bytes'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "staticExtradata", + type: new PrimitiveType("bytes", "bytes", Nothing), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + EIP712_DEF_FIELD_STATICEXTRADATA_BYTES, + ); + }); + + it("should return the apdu for 'replacementPattern' of type 'bytes'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "replacementPattern", + type: new PrimitiveType("bytes", "bytes", Nothing), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + EIP712_DEF_FIELD_REPLACEMENTPATTERN_BYTES, + ); + }); + + it("should return the apdu for 'dataType' of type 'bytes4'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "dataType", + type: new PrimitiveType("bytes4", "bytes", Just(4)), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + EIP712_DEF_FIELD_DATA_TYPE_BYTES4, + ); + }); + + it("should return the apdu for 'document' of type 'string[3][]'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "document", + type: new ArrayType( + "string[3][]", + new PrimitiveType("string", "string", Nothing), + "string[3]", + Nothing, + [Just(3), Nothing], + ), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_FIELD_DOCUMENT_STRING); + }); + + it("should return the apdu for 'depthy' of type 'uint8[][][][]'", () => { + // GIVEN + command = new SendEIP712StructDefinitionCommand({ + command: StructDefinitionCommand.Field, + name: "depthy", + type: new ArrayType( + "uint8[][][][]", + new PrimitiveType("uint8", "uint", Just(1)), + "uint8[][][]", + Nothing, + [Nothing, Nothing, Nothing, Nothing], + ), + }); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual(EIP712_DEF_FIELD_DEPTHY_UINT8); + }); + }); + + describe("parseResponse", () => { + it("should parse the response", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: new Uint8Array(), + }; + + // WHEN + const parsedResponse = command.parseResponse(response); + + // THEN + expect(parsedResponse).toBeUndefined(); + }); + + it("should throw an error if the response is not successful", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x55, 0x15]), + data: new Uint8Array(), + }; + + // WHEN + const promise = () => command.parseResponse(response); + + // THEN + expect(() => { + promise(); + }).toThrow(InvalidStatusWordError); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructDefinitionCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructDefinitionCommand.ts index 18f0e558a..de8b6f34c 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructDefinitionCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructDefinitionCommand.ts @@ -1,2 +1,168 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#eip712-send-struct-definition -export class SendEIP712StructDefinitionCommand {} +import { + Apdu, + ApduBuilder, + type ApduBuilderArgs, + ApduParser, + ApduResponse, + type Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; +import { Just, Maybe, Nothing } from "purify-ts"; + +import { + ArrayType, + type FieldName, + type FieldType, + PrimitiveType, + StructType, +} from "@internal/typed-data/model/Types"; + +export enum StructDefinitionCommand { + Name = 0, + Field = 255, +} + +export type SendEIP712StructDefinitionCommandArgs = + | { command: StructDefinitionCommand.Name; name: string } + | { + command: StructDefinitionCommand.Field; + name: FieldName; + type: FieldType; + }; + +enum ArraySize { + Dynamic, + Fixed, +} + +enum Type { + Custom, + Int, + Uint, + Address, + Bool, + String, + FixedSizedBytes, + DynamicSizedBytes, +} + +export class SendEIP712StructDefinitionCommand + implements Command +{ + constructor(private args: SendEIP712StructDefinitionCommandArgs) {} + + getApdu(): Apdu { + const SendEIP712StructDefinitionArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x1a, + p1: 0x00, + p2: this.args.command, + }; + + // Struct name + if (this.args.command === StructDefinitionCommand.Name) { + return new ApduBuilder(SendEIP712StructDefinitionArgs) + .addAsciiStringToData(this.args.name) + .build(); + } + + // Struct field + const builder = new ApduBuilder(SendEIP712StructDefinitionArgs); + + const typeDesc = this.constructTypeDescByte(this.args.type); + + // Add type descriptor + builder.add8BitUIntToData(typeDesc); + + // Add struct name if this is a custom type + this.getTypeCustomName(this.args.type).ifJust((customName) => { + builder.encodeInLVFromAscii(customName); + }); + + // Add type size, if applicable + this.getTypeSize(this.args.type).ifJust((size) => { + builder.add8BitUIntToData(size); + }); + + // Add array levels, if it is an array + if (this.args.type instanceof ArrayType) { + builder.add8BitUIntToData(this.args.type.levels.length); + for (const level of this.args.type.levels) { + level.caseOf({ + Just: (l) => { + builder.add8BitUIntToData(ArraySize.Fixed).add8BitUIntToData(l); + }, + Nothing: () => { + builder.add8BitUIntToData(ArraySize.Dynamic); + }, + }); + } + } + + // Add field name + return builder.encodeInLVFromAscii(this.args.name).build(); + } + + parseResponse(response: ApduResponse): void { + const parser = new ApduParser(response); + + // TODO: handle the error correctly using a generic error handler + if (!CommandUtils.isSuccessResponse(response)) { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + response.statusCode, + )}`, + ); + } + } + + private constructTypeDescByte(type: FieldType): number { + const isArrayBit = type instanceof ArrayType ? 1 : 0; + const hasTypeSize = this.getTypeSize(type).isJust() ? 1 : 0; + const typeBits = this.getType(type); + + // Combine the bits using bitwise operations + const combinedBits = (isArrayBit << 7) | (hasTypeSize << 6) | typeBits; + return combinedBits; + } + + private getTypeSize(type: FieldType): Maybe { + if (type instanceof ArrayType) { + return this.getTypeSize(type.rootType); + } + return type instanceof PrimitiveType ? type.size : Nothing; + } + + private getTypeCustomName(type: FieldType): Maybe { + if (type instanceof ArrayType) { + return this.getTypeCustomName(type.rootType); + } + return type instanceof StructType ? Just(type.typeName) : Nothing; + } + + private getType(type: FieldType): Type { + if (type instanceof ArrayType) { + return this.getType(type.rootType); + } else if (type instanceof StructType) { + return Type.Custom; + } + switch (type.name) { + case "int": + return Type.Int; + case "uint": + return Type.Uint; + case "address": + return Type.Address; + case "bool": + return Type.Bool; + case "string": + return Type.String; + case "bytes": + return type.size.isJust() + ? Type.FixedSizedBytes + : Type.DynamicSizedBytes; + } + } +} From 7a7113e64707364d3873281cb97f74de06c7a2ae Mon Sep 17 00:00:00 2001 From: Pierre Aoun Date: Wed, 7 Aug 2024 10:21:01 +0200 Subject: [PATCH 37/46] :sparkles: (keyring-eth): Implement SignEIP712Command --- .changeset/brown-lizards-juggle.md | 5 + .../command/SignEIP712Command.test.ts | 109 ++++++++++++++++++ .../app-binder/command/SignEIP712Command.ts | 105 ++++++++++++++++- 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 .changeset/brown-lizards-juggle.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SignEIP712Command.test.ts diff --git a/.changeset/brown-lizards-juggle.md b/.changeset/brown-lizards-juggle.md new file mode 100644 index 000000000..22151dcd9 --- /dev/null +++ b/.changeset/brown-lizards-juggle.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement SignEIP712Command diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignEIP712Command.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignEIP712Command.test.ts new file mode 100644 index 000000000..5d0d772e7 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignEIP712Command.test.ts @@ -0,0 +1,109 @@ +import { + type Command, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; +import { Just, Nothing } from "purify-ts"; + +import { + SignEIP712Command, + SignEIP712CommandResponse, +} from "./SignEIP712Command"; + +const SIGN_EIP712_APDU = Uint8Array.from([ + 0xe0, 0x0c, 0x00, 0x01, 0x15, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x00, + 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]); + +const SIGN_EIP712_APDU_V0 = Uint8Array.from([ + 0xe0, 0x0c, 0x00, 0x00, 0x55, 0x05, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x00, + 0x3c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, +]); + +const LNX_RESPONSE_DATA_GOOD = Uint8Array.from([ + 0x1c, 0x8a, 0x54, 0x05, 0x10, 0xe1, 0x3b, 0x0f, 0x2b, 0x11, 0xa4, 0x51, 0x27, + 0x57, 0x16, 0xd2, 0x9e, 0x08, 0xca, 0xad, 0x07, 0xe8, 0x9a, 0x1c, 0x84, 0x96, + 0x47, 0x82, 0xfb, 0x5e, 0x1a, 0xd7, 0x88, 0x64, 0xa0, 0xde, 0x23, 0x5b, 0x27, + 0x0f, 0xbe, 0x81, 0xe8, 0xe4, 0x06, 0x88, 0xf4, 0xa9, 0xf9, 0xad, 0x9d, 0x28, + 0x3d, 0x69, 0x05, 0x52, 0xc9, 0x33, 0x1d, 0x77, 0x73, 0xce, 0xaf, 0xa5, 0x13, +]); + +const LNX_RESPONSE_GOOD = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: LNX_RESPONSE_DATA_GOOD, +}; + +const LNX_RESPONSE_LOCKED = { + statusCode: Uint8Array.from([0x55, 0x15]), + data: new Uint8Array(), +}; + +const LNX_RESPONSE_DATA_TOO_SHORT = Uint8Array.from([0x01, 0x02]); + +const LNX_RESPONSE_TOO_SHORT = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: LNX_RESPONSE_DATA_TOO_SHORT, +}; + +describe("SignEIP712Command", () => { + let command: Command; + + beforeEach(() => { + command = new SignEIP712Command({ + derivationPath: "44'/60'/0'/0/0", + legacyArgs: Nothing, + }); + }); + + describe("getApdu", () => { + it("should provide the derivation path", () => { + const apdu = command.getApdu(); + expect(apdu.getRawApdu()).toStrictEqual(SIGN_EIP712_APDU); + }); + }); + + describe("parseResponse", () => { + it("should parse the response", () => { + const parsedResponse = command.parseResponse(LNX_RESPONSE_GOOD); + expect(parsedResponse).toStrictEqual({ + v: 0x1c, + r: "0x8a540510e13b0f2b11a451275716d29e08caad07e89a1c84964782fb5e1ad788", + s: "0x64a0de235b270fbe81e8e40688f4a9f9ad9d283d690552c9331d7773ceafa513", + }); + }); + + it("should throw an error if the response is not successful", () => { + expect(() => { + command.parseResponse(LNX_RESPONSE_LOCKED); + }).toThrow(InvalidStatusWordError); + }); + + it("should throw an error if the response is too short", () => { + expect(() => { + command.parseResponse(LNX_RESPONSE_TOO_SHORT); + }).toThrow(InvalidStatusWordError); + }); + }); +}); + +describe("SignEIP712Command V0", () => { + describe("getApdu", () => { + it("should provide the derivation path and hashes", () => { + const command = new SignEIP712Command({ + derivationPath: "44'/60'/0'/0/0", + legacyArgs: Just({ + domainHash: + "0x1111111111111111111111111111111111111111111111111111111111111111", + messageHash: + "0x2222222222222222222222222222222222222222222222222222222222222222", + }), + }); + const apdu = command.getApdu(); + expect(apdu.getRawApdu()).toStrictEqual(SIGN_EIP712_APDU_V0); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SignEIP712Command.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SignEIP712Command.ts index f18b603bb..a1e0d20ca 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SignEIP712Command.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SignEIP712Command.ts @@ -1,2 +1,105 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#sign-eth-eip-712 -export class SignEIP712Command {} +import { + Apdu, + ApduBuilder, + type ApduBuilderArgs, + ApduParser, + ApduResponse, + type Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; +import { Maybe } from "purify-ts"; + +import { type Signature } from "@api/model/Signature"; +import { DerivationPathUtils } from "@internal/shared/utils/DerivationPathUtils"; + +const R_LENGTH = 32; +const S_LENGTH = 32; + +/** + * Legacy implementation parameters. It is now replaced with prior calls to the following commands: + * - SendEIP712StructDefinitionCommand + * - SendEIP712StructImplemCommand + * - SendEIP712FilteringCommand + */ +export type SignEIP712CommandV0Args = { + domainHash: string; + messageHash: string; +}; + +export type SignEIP712CommandArgs = { + readonly derivationPath: string; + readonly legacyArgs: Maybe; +}; + +export type SignEIP712CommandResponse = Signature; + +export class SignEIP712Command + implements Command +{ + constructor(private readonly args: SignEIP712CommandArgs) {} + + getApdu(): Apdu { + const { derivationPath, legacyArgs } = this.args; + + const signEIP712Args: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x0c, + p1: 0x00, + p2: legacyArgs.isJust() ? 0x00 : 0x01, + }; + const paths = DerivationPathUtils.splitPath(derivationPath); + const builder = new ApduBuilder(signEIP712Args); + builder.add8BitUIntToData(paths.length); + for (const path of paths) { + builder.add32BitUIntToData(path); + } + + legacyArgs.ifJust(({ domainHash, messageHash }) => { + builder.addHexaStringToData(domainHash); + builder.addHexaStringToData(messageHash); + }); + + return builder.build(); + } + + parseResponse(apduResponse: ApduResponse): SignEIP712CommandResponse { + const parser = new ApduParser(apduResponse); + + if (!CommandUtils.isSuccessResponse(apduResponse)) { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + apduResponse.statusCode, + )}`, + ); + } + + const v = parser.extract8BitUInt(); + if (!v) { + throw new InvalidStatusWordError("V is missing"); + } + + const r = parser.encodeToHexaString( + parser.extractFieldByLength(R_LENGTH), + true, + ); + if (!r) { + throw new InvalidStatusWordError("R is missing"); + } + + const s = parser.encodeToHexaString( + parser.extractFieldByLength(S_LENGTH), + true, + ); + if (!s) { + throw new InvalidStatusWordError("S is missing"); + } + + return { + r, + s, + v, + }; + } +} From 25cd8f2bd647e5d69783e17178a958eedd1d3836 Mon Sep 17 00:00:00 2001 From: Pierre Aoun Date: Fri, 9 Aug 2024 12:25:47 +0200 Subject: [PATCH 38/46] :sparkles: (keyring-eth): Implement SendEIP712FilteringCommand --- .changeset/brave-squids-obey.md | 5 + .../SendEIP712FilteringCommand.test.ts | 183 ++++++++++++++++++ .../command/SendEIP712FilteringCommand.ts | 101 ++++++++++ 3 files changed, 289 insertions(+) create mode 100644 .changeset/brave-squids-obey.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712FilteringCommand.test.ts create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712FilteringCommand.ts diff --git a/.changeset/brave-squids-obey.md b/.changeset/brave-squids-obey.md new file mode 100644 index 000000000..6480cb3d8 --- /dev/null +++ b/.changeset/brave-squids-obey.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement SendEIP712FilteringCommand diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712FilteringCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712FilteringCommand.test.ts new file mode 100644 index 000000000..d2b3985f3 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712FilteringCommand.test.ts @@ -0,0 +1,183 @@ +import { + ApduResponse, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +import { + Eip712FilterType, + SendEIP712FilteringCommand, + SendEIP712FilteringCommandArgs, +} from "./SendEIP712FilteringCommand"; + +const ACTIVATE_APDU = Uint8Array.from([0xe0, 0x1e, 0x00, 0x00, 0x00]); +const MESSAGE_INFO_APDU = Uint8Array.from([ + 0xe0, 0x1e, 0x00, 0x0f, 0x54, 0x0b, 0x31, 0x69, 0x6e, 0x63, 0x68, 0x20, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x06, 0x46, 0x30, 0x44, 0x02, 0x20, 0x29, 0x5e, 0x0a, + 0xeb, 0x17, 0xca, 0x09, 0x29, 0xb2, 0xa9, 0x4c, 0x32, 0x4d, 0x67, 0xd0, 0xb5, + 0x52, 0x8a, 0xba, 0x26, 0x81, 0x77, 0xf3, 0xac, 0x29, 0x7b, 0x56, 0x31, 0x41, + 0xe0, 0x00, 0x27, 0x02, 0x20, 0x3a, 0xc3, 0x60, 0xd9, 0xfd, 0x0c, 0x9c, 0x0c, + 0x12, 0x27, 0x9d, 0x1e, 0x73, 0xbe, 0xa5, 0xd5, 0x49, 0xa1, 0xe8, 0x14, 0x1f, + 0x45, 0x4d, 0x88, 0xfb, 0xe1, 0xe8, 0xef, 0x97, 0x0e, 0x68, 0x02, +]); +const RAW_APDU = Uint8Array.from([ + 0xe0, 0x1e, 0x00, 0xff, 0x4d, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x47, 0x30, 0x45, + 0x02, 0x21, 0x00, 0xb8, 0x20, 0xe4, 0xdf, 0xb1, 0xa0, 0xcd, 0xe6, 0xdc, 0x97, + 0xd9, 0xa3, 0x4e, 0xeb, 0xb1, 0xa4, 0xee, 0xf0, 0xb2, 0x26, 0x26, 0x2e, 0x67, + 0x88, 0x11, 0x8a, 0xb3, 0xc7, 0xfb, 0x79, 0xfe, 0x35, 0x02, 0x20, 0x2d, 0x42, + 0x6a, 0x38, 0x8b, 0x4c, 0x3a, 0x80, 0x96, 0xb3, 0xf8, 0x44, 0x12, 0xa7, 0x02, + 0xea, 0x53, 0x77, 0x70, 0xe6, 0x1e, 0xe0, 0x72, 0x7e, 0xc1, 0xb7, 0x10, 0xc1, + 0xda, 0x52, 0x0c, 0x44, +]); +const TOKEN_APDU = Uint8Array.from([ + 0xe0, 0x1e, 0x00, 0xfd, 0x49, 0x01, 0x47, 0x30, 0x45, 0x02, 0x21, 0x00, 0xff, + 0x72, 0x78, 0x47, 0x44, 0x54, 0x31, 0xe5, 0x71, 0xcd, 0x2a, 0x0d, 0x9d, 0xb4, + 0x2a, 0x7e, 0xb6, 0x2e, 0x37, 0x87, 0x7b, 0x9b, 0xf2, 0x0e, 0x6a, 0x96, 0x58, + 0x42, 0x55, 0x34, 0x7e, 0x19, 0x02, 0x20, 0x0a, 0x6e, 0x95, 0xb7, 0xf8, 0xe6, + 0x3b, 0x2f, 0xab, 0x0b, 0xef, 0x88, 0xc7, 0x47, 0xde, 0x6a, 0x38, 0x7d, 0x06, + 0x35, 0x1b, 0xe5, 0xbd, 0xc3, 0x4b, 0x2c, 0x1f, 0x9a, 0xea, 0x6f, 0xdd, 0x28, +]); +const AMOUNT_APDU = Uint8Array.from([ + 0xe0, 0x1e, 0x00, 0xfe, 0x59, 0x0f, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, + 0x20, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x01, 0x47, 0x30, 0x45, 0x02, + 0x21, 0x00, 0xa5, 0x9d, 0xc4, 0x79, 0xa8, 0x38, 0xa8, 0x13, 0x90, 0x9c, 0x14, + 0x0a, 0x15, 0xe6, 0xb6, 0x5b, 0xc5, 0x8c, 0x56, 0x33, 0x28, 0x4b, 0xf9, 0x73, + 0xc4, 0x36, 0xde, 0x5a, 0x59, 0x26, 0x34, 0xe2, 0x02, 0x20, 0x1e, 0x03, 0x8f, + 0xc7, 0x99, 0x5d, 0x93, 0x9f, 0xcc, 0xd5, 0x46, 0xe4, 0xc8, 0x5e, 0x79, 0x3c, + 0x0a, 0xd4, 0x51, 0x21, 0x6e, 0x36, 0xa4, 0xed, 0xfc, 0x7b, 0xce, 0x5b, 0xe2, + 0x78, 0x08, 0xcb, +]); +const DATE_TIME_APDU = Uint8Array.from([ + 0xe0, 0x1e, 0x00, 0xfc, 0x58, 0x0f, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, + 0x6c, 0x20, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x47, 0x30, 0x45, 0x02, 0x21, + 0x00, 0xe8, 0x47, 0x16, 0x6e, 0x60, 0xf8, 0x51, 0xe3, 0xc8, 0xd1, 0xf4, 0x41, + 0x39, 0x81, 0x18, 0x98, 0xcc, 0xd0, 0xd3, 0xa0, 0x3a, 0xed, 0x6c, 0x77, 0xf8, + 0xc3, 0x99, 0x38, 0x13, 0xf4, 0x79, 0xd2, 0x02, 0x20, 0x31, 0xfe, 0x6b, 0x6a, + 0x57, 0x4b, 0x56, 0xc5, 0x10, 0x40, 0x03, 0xcf, 0x07, 0x90, 0x0d, 0x11, 0xff, + 0xaf, 0x30, 0x3d, 0xc0, 0x16, 0xda, 0x4c, 0x1c, 0x3d, 0x18, 0x46, 0x63, 0xda, + 0x8f, 0x6a, +]); + +describe("SendEIP712FilteringCommand", () => { + describe("getApdu", () => { + it("Activate APDU", () => { + // GIVEN + const args: SendEIP712FilteringCommandArgs = { + type: Eip712FilterType.Activation, + }; + // WHEN + const command = new SendEIP712FilteringCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(ACTIVATE_APDU); + }); + + it("Message info APDU", () => { + // GIVEN + const args: SendEIP712FilteringCommandArgs = { + type: Eip712FilterType.MessageInfo, + displayName: "1inch Order", + filtersCount: 6, + signature: + "30440220295e0aeb17ca0929b2a94c324d67d0b5528aba268177f3ac297b563141e0002702203ac360d9fd0c9c0c12279d1e73bea5d549a1e8141f454d88fbe1e8ef970e6802", + }; + // WHEN + const command = new SendEIP712FilteringCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(MESSAGE_INFO_APDU); + }); + + it("Raw APDU", () => { + // GIVEN + const args: SendEIP712FilteringCommandArgs = { + type: Eip712FilterType.Raw, + displayName: "From", + signature: + "3045022100b820e4dfb1a0cde6dc97d9a34eebb1a4eef0b226262e6788118ab3c7fb79fe3502202d426a388b4c3a8096b3f84412a702ea537770e61ee0727ec1b710c1da520c44", + }; + // WHEN + const command = new SendEIP712FilteringCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(RAW_APDU); + }); + + it("Token APDU", () => { + // GIVEN + const args: SendEIP712FilteringCommandArgs = { + type: Eip712FilterType.Token, + tokenIndex: 1, + signature: + "3045022100ff727847445431e571cd2a0d9db42a7eb62e37877b9bf20e6a96584255347e1902200a6e95b7f8e63b2fab0bef88c747de6a387d06351be5bdc34b2c1f9aea6fdd28", + }; + // WHEN + const command = new SendEIP712FilteringCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(TOKEN_APDU); + }); + + it("Amount APDU", () => { + // GIVEN + const args: SendEIP712FilteringCommandArgs = { + type: Eip712FilterType.Amount, + displayName: "Receive minimum", + tokenIndex: 1, + signature: + "3045022100a59dc479a838a813909c140a15e6b65bc58c5633284bf973c436de5a592634e202201e038fc7995d939fccd546e4c85e793c0ad451216e36a4edfc7bce5be27808cb", + }; + // WHEN + const command = new SendEIP712FilteringCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(AMOUNT_APDU); + }); + + it("Date-time APDU", () => { + // GIVEN + const args: SendEIP712FilteringCommandArgs = { + type: Eip712FilterType.Datetime, + displayName: "Approval expire", + signature: + "3045022100e847166e60f851e3c8d1f44139811898ccd0d3a03aed6c77f8c3993813f479d2022031fe6b6a574b56c5104003cf07900d11ffaf303dc016da4c1c3d184663da8f6a", + }; + // WHEN + const command = new SendEIP712FilteringCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(DATE_TIME_APDU); + }); + }); + + describe("parseResponse", () => { + it("should throw an error if the response status code is invalid", () => { + // GIVEN + const response: ApduResponse = { + statusCode: Buffer.from([0x6a, 0x80]), // Invalid status code + data: Buffer.from([]), + }; + // WHEN + const command = new SendEIP712FilteringCommand({ + type: Eip712FilterType.Activation, + }); + // THEN + expect(() => command.parseResponse(response)).toThrow( + InvalidStatusWordError, + ); + }); + + it("should parse the response", () => { + // GIVEN + const response: ApduResponse = { + statusCode: Buffer.from([0x90, 0x00]), // Success status code + data: Buffer.from([]), + }; + // WHEN + const command = new SendEIP712FilteringCommand({ + type: Eip712FilterType.Activation, + }); + // THEN + expect(() => command.parseResponse(response)).not.toThrow(); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712FilteringCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712FilteringCommand.ts new file mode 100644 index 000000000..708e1d209 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712FilteringCommand.ts @@ -0,0 +1,101 @@ +// https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#eip712-filtering +import { + Apdu, + ApduBuilder, + type ApduBuilderArgs, + ApduParser, + ApduResponse, + type Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +export enum Eip712FilterType { + Activation = "activation", + MessageInfo = "message_info", + Datetime = "datetime", + Raw = "raw", + Amount = "amount", + Token = "token", +} + +export type SendEIP712FilteringCommandArgs = + | { type: Eip712FilterType.Activation } + | { + type: Eip712FilterType.MessageInfo; + displayName: string; + filtersCount: number; + signature: string; + } + | { type: Eip712FilterType.Datetime; displayName: string; signature: string } + | { type: Eip712FilterType.Token; tokenIndex: number; signature: string } + | { type: Eip712FilterType.Raw; displayName: string; signature: string } + | { + type: Eip712FilterType.Amount; + displayName: string; + tokenIndex: number; + signature: string; + }; + +const FILTER_TO_P2: Record = { + [Eip712FilterType.Activation]: 0x00, + [Eip712FilterType.MessageInfo]: 0x0f, + [Eip712FilterType.Datetime]: 0xfc, + [Eip712FilterType.Token]: 0xfd, + [Eip712FilterType.Amount]: 0xfe, + [Eip712FilterType.Raw]: 0xff, +}; + +export class SendEIP712FilteringCommand + implements Command +{ + constructor(private readonly args: SendEIP712FilteringCommandArgs) {} + + getApdu(): Apdu { + const filteringArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x1e, + p1: 0x00, + p2: FILTER_TO_P2[this.args.type], + }; + const builder = new ApduBuilder(filteringArgs); + + if (this.args.type === Eip712FilterType.MessageInfo) { + builder + .encodeInLVFromAscii(this.args.displayName) + .add8BitUIntToData(this.args.filtersCount) + .encodeInLVFromHexa(this.args.signature); + } else if ( + this.args.type === Eip712FilterType.Datetime || + this.args.type === Eip712FilterType.Raw + ) { + builder + .encodeInLVFromAscii(this.args.displayName) + .encodeInLVFromHexa(this.args.signature); + } else if ( + this.args.type === Eip712FilterType.Token || + this.args.type === Eip712FilterType.Amount + ) { + if (this.args.type === Eip712FilterType.Amount) { + builder.encodeInLVFromAscii(this.args.displayName); + } + builder + .add8BitUIntToData(this.args.tokenIndex) + .encodeInLVFromHexa(this.args.signature); + } + + return builder.build(); + } + + parseResponse(apduResponse: ApduResponse): void { + const parser = new ApduParser(apduResponse); + + if (!CommandUtils.isSuccessResponse(apduResponse)) { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + apduResponse.statusCode, + )}`, + ); + } + } +} From b5d81da73ff4c5ee9fdce231f218dcfd40e28083 Mon Sep 17 00:00:00 2001 From: jiyuzhuang Date: Mon, 12 Aug 2024 17:44:08 +0200 Subject: [PATCH 39/46] =?UTF-8?q?=E2=9C=A8=20(keyring-eth):=20Implement=20?= =?UTF-8?q?SendEIP712StructImplemCommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/late-flies-smile.md | 5 + .../SendEIP712StructImplemCommand.test.ts | 108 ++++++++++++++++++ .../command/SendEIP712StructImplemCommand.ts | 81 ++++++++++++- 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 .changeset/late-flies-smile.md create mode 100644 packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructImplemCommand.test.ts diff --git a/.changeset/late-flies-smile.md b/.changeset/late-flies-smile.md new file mode 100644 index 000000000..f9f58524e --- /dev/null +++ b/.changeset/late-flies-smile.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Implement SendEIP712StructImplemCommand diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructImplemCommand.test.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructImplemCommand.test.ts new file mode 100644 index 000000000..6849e3ba2 --- /dev/null +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructImplemCommand.test.ts @@ -0,0 +1,108 @@ +import { + SendEIP712StructImplemCommand, + type SendEIP712StructImplemCommandArgs, + StructImplemType, +} from "./SendEIP712StructImplemCommand"; + +const ROOT_APDU = Uint8Array.from([ + 0xe0, 0x1c, 0x00, 0x00, 0x06, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, +]); + +const ARRAY_APDU = Uint8Array.from([0xe0, 0x1c, 0x00, 0x0f, 0x01, 0x13]); + +const FIELD_LAST_CHUNK_APDU = Uint8Array.from([ + 0xe0, 0x1c, 0x00, 0xff, 0x06, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04, +]); + +const FIELD_OTHER_CHUNK_APDU = Uint8Array.from([ + 0xe0, 0x1c, 0x01, 0xff, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, +]); + +describe("SendEIP712StructImplemCommand", () => { + describe("getApdu", () => { + it("should return the correct APDU for ROOT", () => { + // GIVEN + const args: SendEIP712StructImplemCommandArgs = { + type: StructImplemType.ROOT, + value: "ledger", + }; + // WHEN + const command = new SendEIP712StructImplemCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(ROOT_APDU); + }); + it("should return the correct APDU for ARRAY", () => { + // GIVEN + const args: SendEIP712StructImplemCommandArgs = { + type: StructImplemType.ARRAY, + value: 19, + }; + // WHEN + const command = new SendEIP712StructImplemCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(ARRAY_APDU); + }); + it("should return the correct APDU for FIELD when receiving last chunk", () => { + // GIVEN + const args: SendEIP712StructImplemCommandArgs = { + type: StructImplemType.FIELD, + value: { + data: Uint8Array.from([0x00, 0x04, 0x01, 0x02, 0x03, 0x04]), + isLastChunk: true, + }, + }; + // WHEN + const command = new SendEIP712StructImplemCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(FIELD_LAST_CHUNK_APDU); + }); + it("should return the correct APDU for FIELD when receiving other chunk", () => { + // GIVEN + const args: SendEIP712StructImplemCommandArgs = { + type: StructImplemType.FIELD, + value: { + data: Uint8Array.from([0x05, 0x06, 0x07, 0x08, 0x09]), + isLastChunk: false, + }, + }; + // WHEN + const command = new SendEIP712StructImplemCommand(args); + const apdu = command.getApdu(); + // THEN + expect(apdu.getRawApdu()).toStrictEqual(FIELD_OTHER_CHUNK_APDU); + }); + }); + describe("parseResponse", () => { + it("should throw an error if the response status code is not success", () => { + // GIVEN + const response = { + data: new Uint8Array(), + statusCode: new Uint8Array([0x6a, 0x80]), + }; + // WHEN + const command = new SendEIP712StructImplemCommand({ + type: StructImplemType.ROOT, + value: "ledger", + }); + // THEN + expect(() => command.parseResponse(response)).toThrow(); + }); + it("should not throw an error if the response status code is success", () => { + // GIVEN + const response = { + data: new Uint8Array(), + statusCode: new Uint8Array([0x90, 0x00]), + }; + // WHEN + const command = new SendEIP712StructImplemCommand({ + type: StructImplemType.ROOT, + value: "ledger", + }); + // THEN + expect(() => command.parseResponse(response)).not.toThrow(); + }); + }); +}); diff --git a/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructImplemCommand.ts b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructImplemCommand.ts index 269b39a68..fbb163f47 100644 --- a/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructImplemCommand.ts +++ b/packages/signer/keyring-eth/src/internal/app-binder/command/SendEIP712StructImplemCommand.ts @@ -1,2 +1,81 @@ // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#eip712-send-struct-implementation -export class SendEIP712StructImplemCommand {} +import { + Apdu, + ApduBuilder, + type ApduBuilderArgs, + ApduParser, + ApduResponse, + type Command, + CommandUtils, + InvalidStatusWordError, +} from "@ledgerhq/device-sdk-core"; + +export enum StructImplemType { + ROOT = 0x00, + ARRAY = 0x0f, + FIELD = 0xff, +} + +export type SendEIP712StructImplemCommandArgs = + | { + type: StructImplemType.ROOT; + value: string; + } + | { + type: StructImplemType.ARRAY; + value: number; + } + | { + type: StructImplemType.FIELD; + value: { + /** + * The chunk of the data that is ready to send, that is to say, prefixed by its length in two bytes. + * Eg. 01020304 => [0x00, 0x04, 0x01, 0x02, 0x03, 0x04] where 0x00, 0x04 are the length of the data. + */ + data: Uint8Array; + isLastChunk: boolean; + }; + }; + +export class SendEIP712StructImplemCommand implements Command { + constructor(private readonly args: SendEIP712StructImplemCommandArgs) {} + + getApdu(): Apdu { + const apduBuilderArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x1c, + p1: + this.args.type != StructImplemType.FIELD || this.args.value.isLastChunk + ? 0x00 + : 0x01, + p2: this.args.type, + }; + switch (this.args.type) { + case StructImplemType.ROOT: + return new ApduBuilder(apduBuilderArgs) + .addAsciiStringToData(this.args.value) + .build(); + case StructImplemType.ARRAY: + return new ApduBuilder(apduBuilderArgs) + .add8BitUIntToData(this.args.value) + .build(); + case StructImplemType.FIELD: + return new ApduBuilder(apduBuilderArgs) + .addBufferToData(this.args.value.data) + .build(); + } + } + + parseResponse(response: ApduResponse): void { + const parser = new ApduParser(response); + + // TODO: handle the error correctly using a generic error handler + if (!CommandUtils.isSuccessResponse(response)) { + throw new InvalidStatusWordError( + `Unexpected status word: ${parser.encodeToHexaString( + response.statusCode, + )}`, + ); + } + } +} From 834f2235dc9f4f43a321a0f52fa46ce7727c649a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:48:10 +0000 Subject: [PATCH 40/46] :arrow_up: (repo) [NO-ISSUE]: Bump @types/node from 20.14.11 to 22.4.0 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.14.11 to 22.4.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 93 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index c1508b3ce..756fb84e6 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.7", "@types/jest": "^29.5.12", - "@types/node": "^20.14.11", + "@types/node": "^22.4.0", "concurrently": "^8.2.2", "danger": "^12.3.3", "eslint": "9.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c8359e15..af06bd0f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^29.5.12 version: 29.5.12 '@types/node': - specifier: ^20.14.11 - version: 20.14.11 + specifier: ^22.4.0 + version: 22.4.0 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -37,7 +37,7 @@ importers: version: 6.2.11 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + version: 29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) prettier: specifier: ^3.3.2 version: 3.3.2 @@ -46,7 +46,7 @@ importers: version: 6.0.1 ts-jest: specifier: ^29.1.5 - version: 29.1.5(@babel/core@7.24.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.4))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3) + version: 29.1.5(@babel/core@7.24.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.4))(jest@29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)))(typescript@5.5.3) tsc-alias: specifier: ^1.8.10 version: 1.8.10 @@ -137,7 +137,7 @@ importers: devDependencies: ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.14.11)(typescript@5.5.3) + version: 10.9.2(@types/node@22.4.0)(typescript@5.5.3) packages/config/prettier: devDependencies: @@ -216,7 +216,7 @@ importers: version: 1.0.6 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.14.11)(typescript@5.5.3) + version: 10.9.2(@types/node@22.4.0)(typescript@5.5.3) packages/signer/context-module: dependencies: @@ -253,7 +253,7 @@ importers: version: link:../../config/typescript ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.14.11)(typescript@5.5.3) + version: 10.9.2(@types/node@22.4.0)(typescript@5.5.3) packages/signer/keyring-eth: dependencies: @@ -299,7 +299,7 @@ importers: version: 7.8.1 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.14.11)(typescript@5.5.3) + version: 10.9.2(@types/node@22.4.0)(typescript@5.5.3) packages/trusted-apps: dependencies: @@ -2378,6 +2378,9 @@ packages: '@types/node@20.14.11': resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==} + '@types/node@22.4.0': + resolution: {integrity: sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -6107,6 +6110,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.6: + resolution: {integrity: sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==} + unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -8186,7 +8192,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -8200,7 +8206,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + jest-config: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -9549,6 +9555,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@22.4.0': + dependencies: + undici-types: 6.19.6 + '@types/parse-json@4.0.2': {} '@types/pg-pool@2.0.4': @@ -10423,13 +10433,13 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - create-jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)): + create-jest@29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + jest-config: 29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -11697,16 +11707,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)): + jest-cli@29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + create-jest: 29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + jest-config: 29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -11716,7 +11726,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)): + jest-config@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)): dependencies: '@babel/core': 7.24.4 '@jest/test-sequencer': 29.7.0 @@ -11742,7 +11752,38 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.14.11 - ts-node: 10.9.2(@types/node@20.14.11)(typescript@5.5.3) + ts-node: 10.9.2(@types/node@22.4.0)(typescript@5.5.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)): + dependencies: + '@babel/core': 7.24.4 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.4) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.7 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.4.0 + ts-node: 10.9.2(@types/node@22.4.0)(typescript@5.5.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -11968,12 +12009,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)): + jest@29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + jest-cli: 29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -13683,11 +13724,11 @@ snapshots: dependencies: typescript: 5.5.3 - ts-jest@29.1.5(@babel/core@7.24.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.4))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3): + ts-jest@29.1.5(@babel/core@7.24.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.4))(jest@29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)))(typescript@5.5.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)) + jest: 29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -13701,14 +13742,14 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.24.4) - ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3): + ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.14.11 + '@types/node': 22.4.0 acorn: 8.11.3 acorn-walk: 8.3.1 arg: 4.1.3 @@ -13800,6 +13841,8 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.19.6: {} + unicode-canonical-property-names-ecmascript@2.0.0: {} unicode-match-property-ecmascript@2.0.0: From 0b70f6fc96032da54db7bdeed3589ad3bfe25994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 08:55:08 +0000 Subject: [PATCH 41/46] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20(context-module):=20?= =?UTF-8?q?Bump=20ethers=20from=205.7.2=20to=206.13.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ethers](https://github.com/ethers-io/ethers.js) from 5.7.2 to 6.13.2. - [Release notes](https://github.com/ethers-io/ethers.js/releases) - [Changelog](https://github.com/ethers-io/ethers.js/blob/main/CHANGELOG.md) - [Commits](https://github.com/ethers-io/ethers.js/compare/v5.7.2...v6.13.2) --- updated-dependencies: - dependency-name: ethers dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/signer/context-module/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/signer/context-module/package.json b/packages/signer/context-module/package.json index d06eadc28..9fc238c2f 100644 --- a/packages/signer/context-module/package.json +++ b/packages/signer/context-module/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "axios": "^1.7.2", - "ethers": "^5.7.2", + "ethers": "^6.13.2", "inversify": "^6.0.2", "purify-ts": "^2.1.0", "reflect-metadata": "^0.2.2" From df637fa727471d0def59c82a54943504d0379824 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Mon, 19 Aug 2024 17:48:58 +0200 Subject: [PATCH 42/46] =?UTF-8?q?=F0=9F=90=9B=20(context-module):=20Adapt?= =?UTF-8?q?=20code=20for=20ethers=20v6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExternalPluginContextLoader.test.ts | 6 ++---- .../domain/ExternalPluginContextLoader.ts | 15 ++++++------- pnpm-lock.yaml | 21 +++++++++++++++++-- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.test.ts b/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.test.ts index 22d9a9a73..88c3c5c04 100644 --- a/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.test.ts +++ b/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.test.ts @@ -1,4 +1,4 @@ -import { Interface } from "ethers/lib/utils"; +import { Interface } from "ethers"; import { Left, Right } from "purify-ts"; import ABI from "@/external-plugin/__tests__/abi.json"; @@ -430,9 +430,7 @@ describe("ExternalPluginContextLoader", () => { expect(result).toEqual([ { type: "error", - error: new Error( - "[ContextModule] ExternalPluginContextLoader: Unable to get address", - ), + error: new RangeError("out of result range"), }, ]); }); diff --git a/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.ts b/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.ts index e7efde1b3..d8f98d26f 100644 --- a/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.ts +++ b/packages/signer/context-module/src/external-plugin/domain/ExternalPluginContextLoader.ts @@ -1,6 +1,5 @@ import { HexaString, isHexaString } from "@ledgerhq/device-sdk-core"; -import { ethers } from "ethers"; -import { Interface } from "ethers/lib/utils"; +import { ethers, Interface } from "ethers"; import { inject, injectable } from "inversify"; import { Either, EitherAsync, Left, Right } from "purify-ts"; @@ -76,7 +75,7 @@ export class ExternalPluginContextLoader implements ContextLoader { // decodedCallData is a Right so we can extract it safely const extractedDecodedCallData = - decodedCallData.extract() as ethers.utils.Result; + decodedCallData.extract() as ethers.Result; // get the token payload for each erc20OfInterest // and return the payload or the error @@ -110,7 +109,7 @@ export class ExternalPluginContextLoader implements ContextLoader { private getTokenPayload( transaction: TransactionContext, erc20Path: string, - decodedCallData: ethers.utils.Result, + decodedCallData: ethers.Result, ) { const address = this.getAddressFromPath(erc20Path, decodedCallData); @@ -128,7 +127,7 @@ export class ExternalPluginContextLoader implements ContextLoader { abi: object[], method: string, data: string, - ): Either { + ): Either { try { const contractInterface = new Interface(abi); return Right(contractInterface.decodeFunctionData(method, data)); @@ -143,9 +142,9 @@ export class ExternalPluginContextLoader implements ContextLoader { private getAddressFromPath( path: string, - decodedCallData: ethers.utils.Result, + decodedCallData: ethers.Result, ): HexaString { - // ethers.utils.Result is a record string, any + // ethers.Result is a record string, any // eslint-disable-next-line @typescript-eslint/no-explicit-any let value: any = decodedCallData; for (const key of path.split(".")) { @@ -155,6 +154,8 @@ export class ExternalPluginContextLoader implements ContextLoader { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access value = value[value.length - 1]; } else { + // This access can throw a RangeError error in case of an invalid key + // but is correctly caught by the liftEither above // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access value = value[key]; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af06bd0f5..e475dba32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -224,8 +224,8 @@ importers: specifier: ^1.7.2 version: 1.7.2 ethers: - specifier: ^5.7.2 - version: 5.7.2 + specifier: ^6.13.2 + version: 6.13.2 inversify: specifier: ^6.0.2 version: 6.0.2 @@ -3537,6 +3537,10 @@ packages: resolution: {integrity: sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A==} engines: {node: '>=14.0.0'} + ethers@6.13.2: + resolution: {integrity: sha512-9VkriTTed+/27BGuY1s0hf441kqwHJ1wtN2edksEtiRvXx+soxRX3iSXTfFqq2+YwrOqbDoTHjIhQnjJRlzKmg==} + engines: {node: '>=14.0.0'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -10913,6 +10917,19 @@ snapshots: - bufferutil - utf-8-validate + ethers@6.13.2: + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 18.15.13 + aes-js: 4.0.0-beta.5 + tslib: 2.4.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + event-target-shim@5.0.1: {} event-target-shim@6.0.2: {} From 283358e027a6c9d4789e86d3db649b8883555d49 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Mon, 19 Aug 2024 11:13:57 +0200 Subject: [PATCH 43/46] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20(keyring-eth):=20Bum?= =?UTF-8?q?p=20ethers=20to=20v6.13.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/signer/keyring-eth/package.json | 6 +++--- pnpm-lock.yaml | 21 ++------------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/signer/keyring-eth/package.json b/packages/signer/keyring-eth/package.json index 63f37483e..ed3a98f9d 100644 --- a/packages/signer/keyring-eth/package.json +++ b/packages/signer/keyring-eth/package.json @@ -43,7 +43,7 @@ }, "dependencies": { "ethers-v5": "npm:ethers@v5", - "ethers-v6": "npm:ethers@v6", + "ethers-v6": "npm:ethers@^6.13.2", "inversify": "^6.0.2", "inversify-logger-middleware": "^3.1.0", "purify-ts": "^2.1.0", @@ -56,8 +56,8 @@ "@ledgerhq/jest-config-dsdk": "workspace:*", "@ledgerhq/prettier-config-dsdk": "workspace:*", "@ledgerhq/tsconfig-dsdk": "workspace:*", - "ts-node": "^10.9.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "ts-node": "^10.9.2" }, "peerDependencies": { "@ledgerhq/context-module": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e475dba32..4069e0aa8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,8 +261,8 @@ importers: specifier: npm:ethers@v5 version: ethers@5.7.2 ethers-v6: - specifier: npm:ethers@v6 - version: ethers@6.13.1 + specifier: npm:ethers@^6.13.2 + version: ethers@6.13.2 inversify: specifier: ^6.0.2 version: 6.0.2 @@ -3533,10 +3533,6 @@ packages: ethers@5.7.2: resolution: {integrity: sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==} - ethers@6.13.1: - resolution: {integrity: sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A==} - engines: {node: '>=14.0.0'} - ethers@6.13.2: resolution: {integrity: sha512-9VkriTTed+/27BGuY1s0hf441kqwHJ1wtN2edksEtiRvXx+soxRX3iSXTfFqq2+YwrOqbDoTHjIhQnjJRlzKmg==} engines: {node: '>=14.0.0'} @@ -10904,19 +10900,6 @@ snapshots: - bufferutil - utf-8-validate - ethers@6.13.1: - dependencies: - '@adraffy/ens-normalize': 1.10.1 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.2 - '@types/node': 18.15.13 - aes-js: 4.0.0-beta.5 - tslib: 2.4.0 - ws: 8.17.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - ethers@6.13.2: dependencies: '@adraffy/ens-normalize': 1.10.1 From bd2a570a785cc6e9959ef95f2107f732404d29c0 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Mon, 19 Aug 2024 11:17:40 +0200 Subject: [PATCH 44/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(keyring-eth):=20Fix?= =?UTF-8?q?=20ethers=20v5=20version=20to=20v5.7.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/signer/keyring-eth/package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/signer/keyring-eth/package.json b/packages/signer/keyring-eth/package.json index ed3a98f9d..5c3631c05 100644 --- a/packages/signer/keyring-eth/package.json +++ b/packages/signer/keyring-eth/package.json @@ -42,7 +42,7 @@ "test:coverage": "pnpm test -- --coverage" }, "dependencies": { - "ethers-v5": "npm:ethers@v5", + "ethers-v5": "npm:ethers@^5.7.2", "ethers-v6": "npm:ethers@^6.13.2", "inversify": "^6.0.2", "inversify-logger-middleware": "^3.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4069e0aa8..c31c2ad37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -258,7 +258,7 @@ importers: packages/signer/keyring-eth: dependencies: ethers-v5: - specifier: npm:ethers@v5 + specifier: npm:ethers@^5.7.2 version: ethers@5.7.2 ethers-v6: specifier: npm:ethers@^6.13.2 From efe51677c3adcd858c497c2ae48061c9cb2ec460 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Mon, 19 Aug 2024 11:19:19 +0200 Subject: [PATCH 45/46] =?UTF-8?q?=F0=9F=94=96=20(chore):=20Changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/old-ads-deny.md | 5 +++++ .changeset/selfish-months-decide.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/old-ads-deny.md create mode 100644 .changeset/selfish-months-decide.md diff --git a/.changeset/old-ads-deny.md b/.changeset/old-ads-deny.md new file mode 100644 index 000000000..485a232cd --- /dev/null +++ b/.changeset/old-ads-deny.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/context-module": minor +--- + +Bump ethers to v6.13.2 diff --git a/.changeset/selfish-months-decide.md b/.changeset/selfish-months-decide.md new file mode 100644 index 000000000..c9211bf08 --- /dev/null +++ b/.changeset/selfish-months-decide.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/keyring-eth": patch +--- + +Bump ethers to v6.13.2 From 207580a04afd9bccd716ce8348b7cab3cd8d7ca6 Mon Sep 17 00:00:00 2001 From: Pierre Aoun Date: Mon, 12 Aug 2024 15:29:30 +0200 Subject: [PATCH 46/46] :sparkles: (context-module): New API to load typed messages filters --- packages/signer/context-module/package.json | 2 + .../context-module/src/ContextModule.ts | 5 + .../src/ContextModuleBuilder.ts | 21 +- .../src/DefaultContextModule.test.ts | 15 +- .../src/DefaultContextModule.ts | 13 + packages/signer/context-module/src/di.ts | 2 + .../shared/model/TypedDataClearSignContext.ts | 44 ++ .../src/shared/model/TypedDataContext.ts | 17 + .../src/typed-data/data/FiltersDto.ts | 41 ++ .../data/HttpTypedDataDataSource.test.ts | 613 ++++++++++++++++++ .../data/HttpTypedDataDataSource.ts | 174 +++++ .../typed-data/data/TypedDataDataSource.ts | 25 + .../typed-data/di/typedDataModuleFactory.ts | 13 + .../src/typed-data/di/typedDataTypes.ts | 4 + .../DefaultTypedDataContextLoader.test.ts | 403 ++++++++++++ .../domain/DefaultTypedDataContextLoader.ts | 136 ++++ .../domain/TypedDataContextLoader.ts | 6 + pnpm-lock.yaml | 16 + 18 files changed, 1547 insertions(+), 3 deletions(-) create mode 100644 packages/signer/context-module/src/shared/model/TypedDataClearSignContext.ts create mode 100644 packages/signer/context-module/src/shared/model/TypedDataContext.ts create mode 100644 packages/signer/context-module/src/typed-data/data/FiltersDto.ts create mode 100644 packages/signer/context-module/src/typed-data/data/HttpTypedDataDataSource.test.ts create mode 100644 packages/signer/context-module/src/typed-data/data/HttpTypedDataDataSource.ts create mode 100644 packages/signer/context-module/src/typed-data/data/TypedDataDataSource.ts create mode 100644 packages/signer/context-module/src/typed-data/di/typedDataModuleFactory.ts create mode 100644 packages/signer/context-module/src/typed-data/di/typedDataTypes.ts create mode 100644 packages/signer/context-module/src/typed-data/domain/DefaultTypedDataContextLoader.test.ts create mode 100644 packages/signer/context-module/src/typed-data/domain/DefaultTypedDataContextLoader.ts create mode 100644 packages/signer/context-module/src/typed-data/domain/TypedDataContextLoader.ts diff --git a/packages/signer/context-module/package.json b/packages/signer/context-module/package.json index 9fc238c2f..a3bc6b792 100644 --- a/packages/signer/context-module/package.json +++ b/packages/signer/context-module/package.json @@ -46,10 +46,12 @@ "@ledgerhq/jest-config-dsdk": "workspace:*", "@ledgerhq/prettier-config-dsdk": "workspace:*", "@ledgerhq/tsconfig-dsdk": "workspace:*", + "@types/crypto-js": "^4.2.2", "ts-node": "^10.9.2" }, "dependencies": { "axios": "^1.7.2", + "crypto-js": "^4.2.0", "ethers": "^6.13.2", "inversify": "^6.0.2", "purify-ts": "^2.1.0", diff --git a/packages/signer/context-module/src/ContextModule.ts b/packages/signer/context-module/src/ContextModule.ts index 92efdbc27..3123e1c15 100644 --- a/packages/signer/context-module/src/ContextModule.ts +++ b/packages/signer/context-module/src/ContextModule.ts @@ -1,7 +1,12 @@ import { ClearSignContext } from "@/shared/model/ClearSignContext"; import { TransactionContext } from "./shared/model/TransactionContext"; +import { type TypedDataClearSignContext } from "./shared/model/TypedDataClearSignContext"; +import { type TypedDataContext } from "./shared/model/TypedDataContext"; export interface ContextModule { getContexts(transaction: TransactionContext): Promise; + getTypedDataFilters( + typedData: TypedDataContext, + ): Promise; } diff --git a/packages/signer/context-module/src/ContextModuleBuilder.ts b/packages/signer/context-module/src/ContextModuleBuilder.ts index 91ea631ca..e3fa42af3 100644 --- a/packages/signer/context-module/src/ContextModuleBuilder.ts +++ b/packages/signer/context-module/src/ContextModuleBuilder.ts @@ -2,12 +2,14 @@ import { externalPluginTypes } from "@/external-plugin/di/externalPluginTypes"; import { forwardDomainTypes } from "@/forward-domain/di/forwardDomainTypes"; import { nftTypes } from "@/nft/di/nftTypes"; import { tokenTypes } from "@/token/di/tokenTypes"; +import { typedDataTypes } from "@/typed-data/di/typedDataTypes"; import { ExternalPluginContextLoader } from "./external-plugin/domain/ExternalPluginContextLoader"; import { ForwardDomainContextLoader } from "./forward-domain/domain/ForwardDomainContextLoader"; import { NftContextLoader } from "./nft/domain/NftContextLoader"; import { ContextLoader } from "./shared/domain/ContextLoader"; import { TokenContextLoader } from "./token/domain/TokenContextLoader"; +import { type TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader"; import { ContextModule } from "./ContextModule"; import { DefaultContextModule } from "./DefaultContextModule"; import { makeContainer } from "./di"; @@ -15,6 +17,7 @@ import { makeContainer } from "./di"; export class ContextModuleBuilder { private customLoaders: ContextLoader[] = []; private defaultLoaders: ContextLoader[] = []; + private typedDataLoader: TypedDataContextLoader; constructor() { const container = makeContainer(); @@ -29,6 +32,9 @@ export class ContextModuleBuilder { container.get(nftTypes.NftContextLoader), container.get(tokenTypes.TokenContextLoader), ]; + this.typedDataLoader = container.get( + typedDataTypes.TypedDataContextLoader, + ); } /** @@ -52,6 +58,16 @@ export class ContextModuleBuilder { return this; } + /** + * Replace the default loader for typed data clear signing contexts + * + * @returns this + */ + withTypedDataLoader(loader: TypedDataContextLoader) { + this.typedDataLoader = loader; + return this; + } + /** * Build the context module * @@ -59,6 +75,9 @@ export class ContextModuleBuilder { */ build(): ContextModule { const loaders = [...this.defaultLoaders, ...this.customLoaders]; - return new DefaultContextModule({ loaders }); + return new DefaultContextModule({ + loaders, + typedDataLoader: this.typedDataLoader, + }); } } diff --git a/packages/signer/context-module/src/DefaultContextModule.test.ts b/packages/signer/context-module/src/DefaultContextModule.test.ts index dccc046f6..fac586af9 100644 --- a/packages/signer/context-module/src/DefaultContextModule.test.ts +++ b/packages/signer/context-module/src/DefaultContextModule.test.ts @@ -1,4 +1,5 @@ import { TransactionContext } from "./shared/model/TransactionContext"; +import type { TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader"; import { DefaultContextModule } from "./DefaultContextModule"; const contextLoaderStubBuilder = () => { @@ -6,12 +7,17 @@ const contextLoaderStubBuilder = () => { }; describe("DefaultContextModule", () => { + const typedDataLoader: TypedDataContextLoader = { load: jest.fn() }; + beforeEach(() => { jest.restoreAllMocks(); }); it("should initialize the context module with all the default loaders", async () => { - const contextModule = new DefaultContextModule({ loaders: [] }); + const contextModule = new DefaultContextModule({ + loaders: [], + typedDataLoader, + }); const res = await contextModule.getContexts({} as TransactionContext); @@ -19,7 +25,10 @@ describe("DefaultContextModule", () => { }); it("should return an empty array when no loaders", async () => { - const contextModule = new DefaultContextModule({ loaders: [] }); + const contextModule = new DefaultContextModule({ + loaders: [], + typedDataLoader, + }); const res = await contextModule.getContexts({} as TransactionContext); @@ -30,6 +39,7 @@ describe("DefaultContextModule", () => { const loader = contextLoaderStubBuilder(); const contextModule = new DefaultContextModule({ loaders: [loader, loader], + typedDataLoader, }); await contextModule.getContexts({} as TransactionContext); @@ -52,6 +62,7 @@ describe("DefaultContextModule", () => { .mockResolvedValueOnce(responses[1]); const contextModule = new DefaultContextModule({ loaders: [loader, loader], + typedDataLoader, }); const res = await contextModule.getContexts({} as TransactionContext); diff --git a/packages/signer/context-module/src/DefaultContextModule.ts b/packages/signer/context-module/src/DefaultContextModule.ts index 3e3c83957..c76243216 100644 --- a/packages/signer/context-module/src/DefaultContextModule.ts +++ b/packages/signer/context-module/src/DefaultContextModule.ts @@ -1,17 +1,24 @@ +import type { TypedDataClearSignContext } from "@/shared/model/TypedDataClearSignContext"; +import type { TypedDataContext } from "@/shared/model/TypedDataContext"; + import { ContextLoader } from "./shared/domain/ContextLoader"; import { ClearSignContext } from "./shared/model/ClearSignContext"; import { TransactionContext } from "./shared/model/TransactionContext"; +import type { TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader"; import { ContextModule } from "./ContextModule"; type DefaultContextModuleConstructorArgs = { loaders: ContextLoader[]; + typedDataLoader: TypedDataContextLoader; }; export class DefaultContextModule implements ContextModule { private _loaders: ContextLoader[]; + private _typedDataLoader: TypedDataContextLoader; constructor(args: DefaultContextModuleConstructorArgs) { this._loaders = args.loaders; + this._typedDataLoader = args.typedDataLoader; } public async getContexts( @@ -21,4 +28,10 @@ export class DefaultContextModule implements ContextModule { const responses = await Promise.all(promises); return responses.flat(); } + + public async getTypedDataFilters( + typedData: TypedDataContext, + ): Promise { + return this._typedDataLoader.load(typedData); + } } diff --git a/packages/signer/context-module/src/di.ts b/packages/signer/context-module/src/di.ts index 1ba7ffbeb..0edca8670 100644 --- a/packages/signer/context-module/src/di.ts +++ b/packages/signer/context-module/src/di.ts @@ -4,6 +4,7 @@ import { externalPluginModuleFactory } from "@/external-plugin/di/externalPlugin import { forwardDomainModuleFactory } from "@/forward-domain/di/forwardDomainModuleFactory"; import { nftModuleFactory } from "@/nft/di/nftModuleFactory"; import { tokenModuleFactory } from "@/token/di/tokenModuleFactory"; +import { typedDataModuleFactory } from "@/typed-data/di/typedDataModuleFactory"; export const makeContainer = () => { const container = new Container(); @@ -13,6 +14,7 @@ export const makeContainer = () => { forwardDomainModuleFactory(), nftModuleFactory(), tokenModuleFactory(), + typedDataModuleFactory(), ); return container; diff --git a/packages/signer/context-module/src/shared/model/TypedDataClearSignContext.ts b/packages/signer/context-module/src/shared/model/TypedDataClearSignContext.ts new file mode 100644 index 000000000..a42269ae0 --- /dev/null +++ b/packages/signer/context-module/src/shared/model/TypedDataClearSignContext.ts @@ -0,0 +1,44 @@ +// The general informations for a typed message +export type TypedDataMessageInfo = { + displayName: string; + filtersCount: number; + signature: string; +}; + +// Token index and descriptor. Needed for tokens that are referenced by a typed message +export type TypedDataTokenIndex = number; +export type TypedDataToken = string; +// Special token index value when the referenced token is the verifying contract +export const VERIFYING_CONTRACT_TOKEN_INDEX = 255; + +// Typed message filters, to select fields to display, and provide formatting informations +export type TypedDataFilterPath = string; +export type TypedDataFilter = + | { + type: "datetime" | "raw"; + displayName: string; + path: TypedDataFilterPath; + signature: string; + } + | { + type: "amount" | "token"; + displayName: string; + tokenIndex: TypedDataTokenIndex; + path: TypedDataFilterPath; + signature: string; + }; + +// Clear signing context for a typed message +export type TypedDataClearSignContextSuccess = { + type: "success"; + messageInfo: TypedDataMessageInfo; + filters: Record; + tokens: Record; +}; +export type TypedDataClearSignContextError = { + type: "error"; + error: Error; +}; +export type TypedDataClearSignContext = + | TypedDataClearSignContextSuccess + | TypedDataClearSignContextError; diff --git a/packages/signer/context-module/src/shared/model/TypedDataContext.ts b/packages/signer/context-module/src/shared/model/TypedDataContext.ts new file mode 100644 index 000000000..5f101ed43 --- /dev/null +++ b/packages/signer/context-module/src/shared/model/TypedDataContext.ts @@ -0,0 +1,17 @@ +// The schema of a typed message +export type TypedDataSchema = Record< + string, + Array<{ name: string; type: string }> +>; + +// The extracted message values, with their path +export type TypedDataFieldValues = Array<{ path: string; value: Uint8Array }>; + +// Context needed to fetch the clear signing context of a typed message +export type TypedDataContext = { + verifyingContract: string; + chainId: number; + version: "v1" | "v2"; + schema: TypedDataSchema; + fieldsValues: TypedDataFieldValues; +}; diff --git a/packages/signer/context-module/src/typed-data/data/FiltersDto.ts b/packages/signer/context-module/src/typed-data/data/FiltersDto.ts new file mode 100644 index 000000000..ef3377b61 --- /dev/null +++ b/packages/signer/context-module/src/typed-data/data/FiltersDto.ts @@ -0,0 +1,41 @@ +export type FilterFieldV1 = { + label: string; + path: string; + signature: string; + format?: never; +}; + +export type FilterFieldV2 = { + label: string; + path: string; + signature: string; + format: "raw" | "datetime"; + coin_ref?: never; +}; + +export type FilterFieldV2WithCoinRef = { + label: string; + path: string; + signature: string; + format: "token" | "amount"; + coin_ref: number; +}; + +export type FilterField = + | FilterFieldV1 + | FilterFieldV2 + | FilterFieldV2WithCoinRef; + +export type FiltersDto = { + eip712_signatures: { + [contractAddress: string]: { + [schemaHash: string]: { + contractName: { + label: string; + signature: string; + }; + fields: Array; + }; + }; + }; +}; diff --git a/packages/signer/context-module/src/typed-data/data/HttpTypedDataDataSource.test.ts b/packages/signer/context-module/src/typed-data/data/HttpTypedDataDataSource.test.ts new file mode 100644 index 000000000..8c4154d63 --- /dev/null +++ b/packages/signer/context-module/src/typed-data/data/HttpTypedDataDataSource.test.ts @@ -0,0 +1,613 @@ +import axios from "axios"; +import { Right } from "purify-ts"; + +import { HttpTypedDataDataSource } from "@/typed-data/data/HttpTypedDataDataSource"; +import { type TypedDataDataSource } from "@/typed-data/data/TypedDataDataSource"; +import PACKAGE from "@root/package.json"; + +jest.mock("axios"); + +describe("HttpTypedDataDataSource", () => { + let datasource: TypedDataDataSource; + + const TEST_TYPES = { + PermitSingle: [ + { + name: "details", + type: "PermitDetails", + }, + { + name: "spender", + type: "address", + }, + { + name: "sigDeadline", + type: "uint256", + }, + ], + PermitDetails: [ + { + type: "address", + name: "token", + }, + { + name: "amount", + type: "uint160", + }, + { + name: "expiration", + type: "uint48", + }, + { + name: "nonce", + type: "uint48", + }, + ], + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + }; + + beforeAll(() => { + datasource = new HttpTypedDataDataSource(); + jest.clearAllMocks(); + }); + + it("should call axios with the ledger client version header", async () => { + // GIVEN + const version = `context-module/${PACKAGE.version}`; + const requestSpy = jest.fn(() => Promise.resolve({ data: [] })); + jest.spyOn(axios, "request").mockImplementation(requestSpy); + + // WHEN + await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x00", + version: "v2", + schema: {}, + }); + + // THEN + expect(requestSpy).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { "X-Ledger-Client-Version": version }, + }), + ); + }); + + it("should return V2 filters when axios response is correct", async () => { + // GIVEN + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: [ + { + coin_ref: 0, + format: "token", + label: "Amount allowance", + path: "details.token", + signature: + "3044022075103b38995e031d1ebbfe38ac6603bec32854b5146a664e49b4cc4f460c1da6022029f4b0fd1f3b7995ffff1627d4b57f27888a2dcc9b3a4e85c37c67571092c733", + }, + { + coin_ref: 0, + format: "amount", + label: "Amount allowance", + path: "details.amount", + signature: + "304402201a46e6b4ef89eaf9fcf4945d053bfc5616a826400fd758312fbbe976bafc07ec022025a9b408722baf983ee053f90179c75b0c55bb0668f437d55493e36069bbd5a3", + }, + { + format: "raw", + label: "Approve to spender", + path: "spender", + signature: + "3044022033e5713d9cb9bc375b56a9fb53b736c81ea3c4ac5cfb2d3ca7f8b8f0558fe2430220543ca4fef6d6f725f29e343f167fe9dd582aa856ecb5797259050eb990a1befb", + }, + { + format: "datetime", + label: "Approval expire", + path: "details.expiration", + signature: + "3044022056b3381e4540629ad73bc434ec49d80523234b82f62340fbb77157fb0eb21a680220459fe9cf6ca309f9c7dfc6d4711fea1848dba661563c57f77b3c2dc480b3a63b", + }, + ], + }, + }, + }, + }, + ]; + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v2", + schema: TEST_TYPES, + }); + + // THEN + expect(result).toEqual( + Right({ + messageInfo: { + displayName: "Permit2", + filtersCount: 4, + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + filters: [ + { + type: "token", + displayName: "Amount allowance", + path: "details.token", + tokenIndex: 0, + signature: + "3044022075103b38995e031d1ebbfe38ac6603bec32854b5146a664e49b4cc4f460c1da6022029f4b0fd1f3b7995ffff1627d4b57f27888a2dcc9b3a4e85c37c67571092c733", + }, + { + type: "amount", + displayName: "Amount allowance", + path: "details.amount", + tokenIndex: 0, + signature: + "304402201a46e6b4ef89eaf9fcf4945d053bfc5616a826400fd758312fbbe976bafc07ec022025a9b408722baf983ee053f90179c75b0c55bb0668f437d55493e36069bbd5a3", + }, + { + type: "raw", + displayName: "Approve to spender", + path: "spender", + signature: + "3044022033e5713d9cb9bc375b56a9fb53b736c81ea3c4ac5cfb2d3ca7f8b8f0558fe2430220543ca4fef6d6f725f29e343f167fe9dd582aa856ecb5797259050eb990a1befb", + }, + { + type: "datetime", + displayName: "Approval expire", + path: "details.expiration", + signature: + "3044022056b3381e4540629ad73bc434ec49d80523234b82f62340fbb77157fb0eb21a680220459fe9cf6ca309f9c7dfc6d4711fea1848dba661563c57f77b3c2dc480b3a63b", + }, + ], + }), + ); + }); + + it("should return V1 filters when axios response is correct", async () => { + // GIVEN + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: [ + { + label: "Amount allowance", + path: "details.token", + signature: + "3045022100c98bae217208d9ba8e3649163d8ee9ed2f69518b4ab7204dba15eda4b3ff32aa02205f03f9a6fac8ae4eceb6b61703bfd7f27f58a83bf21b2f815aec2ad766ba7009", + }, + { + label: "Amount allowance", + path: "details.amount", + signature: + "3045022100bb9bb0c71678a39ba8ed764a67bae0998b992850b7dd1dfefc2fbb7cf6036b170220041568fbd2f58b4cca4012a48ab3b4ddab54fbbc5280fe854ec92ca92dcd9ded", + }, + { + label: "Approve to spender", + path: "spender", + signature: + "3044022033e5713d9cb9bc375b56a9fb53b736c81ea3c4ac5cfb2d3ca7f8b8f0558fe2430220543ca4fef6d6f725f29e343f167fe9dd582aa856ecb5797259050eb990a1befb", + }, + { + label: "Approval expire", + path: "details.expiration", + signature: + "304502210094deb9cc390f9a507ace0c3b32a33c1a3388960f673e8f4fe019b203c3c4918902206363885ee3b37fe441b50a47de18ae2a4feddf001454dbb93a3800565cc11fa9", + }, + ], + }, + }, + }, + }, + ]; + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result).toEqual( + Right({ + messageInfo: { + displayName: "Permit2", + filtersCount: 4, + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + filters: [ + { + type: "raw", + displayName: "Amount allowance", + path: "details.token", + signature: + "3045022100c98bae217208d9ba8e3649163d8ee9ed2f69518b4ab7204dba15eda4b3ff32aa02205f03f9a6fac8ae4eceb6b61703bfd7f27f58a83bf21b2f815aec2ad766ba7009", + }, + { + type: "raw", + displayName: "Amount allowance", + path: "details.amount", + signature: + "3045022100bb9bb0c71678a39ba8ed764a67bae0998b992850b7dd1dfefc2fbb7cf6036b170220041568fbd2f58b4cca4012a48ab3b4ddab54fbbc5280fe854ec92ca92dcd9ded", + }, + { + type: "raw", + displayName: "Approve to spender", + path: "spender", + signature: + "3044022033e5713d9cb9bc375b56a9fb53b736c81ea3c4ac5cfb2d3ca7f8b8f0558fe2430220543ca4fef6d6f725f29e343f167fe9dd582aa856ecb5797259050eb990a1befb", + }, + { + type: "raw", + displayName: "Approval expire", + path: "details.expiration", + signature: + "304502210094deb9cc390f9a507ace0c3b32a33c1a3388960f673e8f4fe019b203c3c4918902206363885ee3b37fe441b50a47de18ae2a4feddf001454dbb93a3800565cc11fa9", + }, + ], + }), + ); + }); + + it("should return an error when data is empty", async () => { + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: undefined }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error when schema is not found", async () => { + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df4": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: [], + }, + }, + }, + }, + ]; + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error if message info is invalid", async () => { + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: "should be an array", + }, + }, + }, + }, + ]; + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error if field is invalid", async () => { + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: ["should be an object"], + }, + }, + }, + }, + ]; + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error if field path is invalid", async () => { + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: [ + { + label: "Amount allowance", + path: 2, + signature: + "3045022100c98bae217208d9ba8e3649163d8ee9ed2f69518b4ab7204dba15eda4b3ff32aa02205f03f9a6fac8ae4eceb6b61703bfd7f27f58a83bf21b2f815aec2ad766ba7009", + }, + ], + }, + }, + }, + }, + ]; + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error if field label is invalid", async () => { + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: [ + { + label: 2, + path: "details.token", + signature: + "3045022100c98bae217208d9ba8e3649163d8ee9ed2f69518b4ab7204dba15eda4b3ff32aa02205f03f9a6fac8ae4eceb6b61703bfd7f27f58a83bf21b2f815aec2ad766ba7009", + }, + ], + }, + }, + }, + }, + ]; + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error if field signature is invalid", async () => { + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: [ + { + label: "Amount allowance", + path: "details.token", + signature: 2, + }, + ], + }, + }, + }, + }, + ]; + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error on raw fields with coin ref", async () => { + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: [ + { + format: "raw", + label: "Amount allowance", + path: "details.token", + coin_ref: 0, + signature: + "3045022100c98bae217208d9ba8e3649163d8ee9ed2f69518b4ab7204dba15eda4b3ff32aa02205f03f9a6fac8ae4eceb6b61703bfd7f27f58a83bf21b2f815aec2ad766ba7009", + }, + ], + }, + }, + }, + }, + ]; + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error on token fields without coin ref", async () => { + const filtersDTO = [ + { + eip712_signatures: { + "0x000000000022d473030f116ddee9f6b43ac78ba3": { + "4d593149e876e739220f3b5ede1b38a0213d76c4705b1547c4323df3": { + contractName: { + label: "Permit2", + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + fields: [ + { + format: "token", + label: "Amount allowance", + path: "details.token", + signature: + "3045022100c98bae217208d9ba8e3649163d8ee9ed2f69518b4ab7204dba15eda4b3ff32aa02205f03f9a6fac8ae4eceb6b61703bfd7f27f58a83bf21b2f815aec2ad766ba7009", + }, + ], + }, + }, + }, + }, + ]; + // GIVEN + jest.spyOn(axios, "request").mockResolvedValue({ data: filtersDTO }); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); + + it("should return an error when axios throws an error", async () => { + // GIVEN + jest.spyOn(axios, "request").mockRejectedValue(new Error()); + + // WHEN + const result = await datasource.getTypedDataFilters({ + chainId: 1, + address: "0x000000000022d473030f116ddee9f6b43ac78ba3", + version: "v1", + schema: TEST_TYPES, + }); + + // THEN + expect(result.isLeft()).toEqual(true); + }); +}); diff --git a/packages/signer/context-module/src/typed-data/data/HttpTypedDataDataSource.ts b/packages/signer/context-module/src/typed-data/data/HttpTypedDataDataSource.ts new file mode 100644 index 000000000..5253e1f0a --- /dev/null +++ b/packages/signer/context-module/src/typed-data/data/HttpTypedDataDataSource.ts @@ -0,0 +1,174 @@ +import axios from "axios"; +import SHA224 from "crypto-js/sha224"; +import { injectable } from "inversify"; +import { Either, Left, Right } from "purify-ts"; + +import type { + TypedDataFilter, + TypedDataMessageInfo, +} from "@/shared/model/TypedDataClearSignContext"; +import type { TypedDataSchema } from "@/shared/model/TypedDataContext"; +import PACKAGE from "@root/package.json"; + +import type { + FilterField, + FilterFieldV1, + FilterFieldV2, + FilterFieldV2WithCoinRef, + FiltersDto, +} from "./FiltersDto"; +import { + GetTypedDataFiltersParams, + GetTypedDataFiltersResult, + TypedDataDataSource, +} from "./TypedDataDataSource"; + +@injectable() +export class HttpTypedDataDataSource implements TypedDataDataSource { + public async getTypedDataFilters({ + chainId, + address, + schema, + version, + }: GetTypedDataFiltersParams): Promise< + Either + > { + try { + const response = await axios.request({ + method: "GET", + url: `https://crypto-assets-service.api.ledger.com/v1/dapps`, + params: { + contracts: address, + chain_id: chainId, + output: "eip712_signatures", + eip712_signatures_version: version, + }, + headers: { + "X-Ledger-Client-Version": `context-module/${PACKAGE.version}`, + }, + }); + + // Try to get the filters JSON descriptor, from address and schema hash + const schemaHash = SHA224( + JSON.stringify(this.sortTypes(schema)).replace(" ", ""), + ).toString(); + const filtersJson = + response.data?.[0]?.eip712_signatures?.[address]?.[schemaHash]; + if (!filtersJson) { + return Left( + new Error( + `[ContextModule] HttpTypedDataDataSource: no typed data filters for address ${address} on chain ${chainId} for schema ${schemaHash}`, + ), + ); + } + + // Parse the message type, if available + if ( + !filtersJson.contractName || + typeof filtersJson.contractName.label !== "string" || + typeof filtersJson.contractName.signature !== "string" || + !Array.isArray(filtersJson.fields) + ) { + return Left( + new Error( + `[ContextModule] HttpTypedDataDataSource: no message info for address ${address} on chain ${chainId} for schema ${schemaHash}`, + ), + ); + } + const messageInfo: TypedDataMessageInfo = { + displayName: filtersJson.contractName.label, + filtersCount: filtersJson.fields.length, + signature: filtersJson.contractName.signature, + }; + + // Parse all the filters + const filters: TypedDataFilter[] = []; + for (const field of filtersJson.fields) { + if (this.isFieldFilterV1(field)) { + filters.push({ + type: "raw", + displayName: field.label, + path: field.path, + signature: field.signature, + }); + } else if (this.isFieldFilterV2(field)) { + filters.push({ + type: field.format, + displayName: field.label, + path: field.path, + signature: field.signature, + }); + } else if (this.isFieldFilterV2WithCoinRef(field)) { + filters.push({ + type: field.format, + displayName: field.label, + path: field.path, + signature: field.signature, + tokenIndex: field.coin_ref, + }); + } else { + return Left( + new Error( + `[ContextModule] HttpTypedDataDataSource: invalid typed data field for address ${address} on chain ${chainId} for schema ${schemaHash}`, + ), + ); + } + } + + return Right({ messageInfo, filters }); + } catch (error) { + return Left( + new Error( + "[ContextModule] HttpTypedDataDataSource: Failed to fetch typed data informations", + ), + ); + } + } + + private isFieldFilterV1(data: FilterField): data is FilterFieldV1 { + return ( + typeof data === "object" && + typeof data.label === "string" && + typeof data.path === "string" && + typeof data.signature === "string" && + (data.format === undefined || data.format === null) + ); + } + + private isFieldFilterV2(data: FilterField): data is FilterFieldV2 { + return ( + typeof data === "object" && + typeof data.label === "string" && + typeof data.path === "string" && + typeof data.signature === "string" && + typeof data.format === "string" && + ["raw", "datetime"].includes(data.format) && + (data.coin_ref === undefined || data.coin_ref === null) + ); + } + + private isFieldFilterV2WithCoinRef( + data: FilterField, + ): data is FilterFieldV2WithCoinRef { + return ( + typeof data === "object" && + typeof data.label === "string" && + typeof data.path === "string" && + typeof data.signature === "string" && + typeof data.format === "string" && + ["token", "amount"].includes(data.format) && + typeof data.coin_ref === "number" + ); + } + + private sortTypes(types: TypedDataSchema): TypedDataSchema { + return Object.fromEntries( + Object.entries(types) + .sort(([aKey], [bKey]) => aKey.localeCompare(bKey)) + .map(([key, value]) => [ + key, + value.map((v) => ({ name: v.name, type: v.type })), + ]), + ); + } +} diff --git a/packages/signer/context-module/src/typed-data/data/TypedDataDataSource.ts b/packages/signer/context-module/src/typed-data/data/TypedDataDataSource.ts new file mode 100644 index 000000000..e80f7c59f --- /dev/null +++ b/packages/signer/context-module/src/typed-data/data/TypedDataDataSource.ts @@ -0,0 +1,25 @@ +import { Either } from "purify-ts"; + +import { + TypedDataFilter, + TypedDataMessageInfo, +} from "@/shared/model/TypedDataClearSignContext"; +import { TypedDataSchema } from "@/shared/model/TypedDataContext"; + +export type GetTypedDataFiltersParams = { + address: string; + chainId: number; + version: "v1" | "v2"; + schema: TypedDataSchema; +}; + +export type GetTypedDataFiltersResult = { + messageInfo: TypedDataMessageInfo; + filters: TypedDataFilter[]; +}; + +export interface TypedDataDataSource { + getTypedDataFilters( + params: GetTypedDataFiltersParams, + ): Promise>; +} diff --git a/packages/signer/context-module/src/typed-data/di/typedDataModuleFactory.ts b/packages/signer/context-module/src/typed-data/di/typedDataModuleFactory.ts new file mode 100644 index 000000000..1b1fb2cb8 --- /dev/null +++ b/packages/signer/context-module/src/typed-data/di/typedDataModuleFactory.ts @@ -0,0 +1,13 @@ +import { ContainerModule } from "inversify"; + +import { HttpTypedDataDataSource } from "@/typed-data/data/HttpTypedDataDataSource"; +import { typedDataTypes } from "@/typed-data/di/typedDataTypes"; +import { DefaultTypedDataContextLoader } from "@/typed-data/domain/DefaultTypedDataContextLoader"; + +export const typedDataModuleFactory = () => + new ContainerModule((bind, _unbind, _isBound, _rebind) => { + bind(typedDataTypes.TypedDataDataSource).to(HttpTypedDataDataSource); + bind(typedDataTypes.TypedDataContextLoader).to( + DefaultTypedDataContextLoader, + ); + }); diff --git a/packages/signer/context-module/src/typed-data/di/typedDataTypes.ts b/packages/signer/context-module/src/typed-data/di/typedDataTypes.ts new file mode 100644 index 000000000..43538e9fd --- /dev/null +++ b/packages/signer/context-module/src/typed-data/di/typedDataTypes.ts @@ -0,0 +1,4 @@ +export const typedDataTypes = { + TypedDataDataSource: Symbol.for("TypedDataDataSource"), + TypedDataContextLoader: Symbol.for("TypedDataContextLoader"), +}; diff --git a/packages/signer/context-module/src/typed-data/domain/DefaultTypedDataContextLoader.test.ts b/packages/signer/context-module/src/typed-data/domain/DefaultTypedDataContextLoader.test.ts new file mode 100644 index 000000000..49a48124e --- /dev/null +++ b/packages/signer/context-module/src/typed-data/domain/DefaultTypedDataContextLoader.test.ts @@ -0,0 +1,403 @@ +import { Left, Right } from "purify-ts"; + +import type { TypedDataContext } from "@/shared/model/TypedDataContext"; +import type { TokenDataSource } from "@/token/data/TokenDataSource"; +import type { TypedDataDataSource } from "@/typed-data/data/TypedDataDataSource"; +import { DefaultTypedDataContextLoader } from "@/typed-data/domain/DefaultTypedDataContextLoader"; + +describe("TokenContextLoader", () => { + const mockTokenDataSource: TokenDataSource = { + getTokenInfosPayload: jest.fn(), + }; + const mockTypedDataDataSource: TypedDataDataSource = { + getTypedDataFilters: jest.fn(), + }; + const loader = new DefaultTypedDataContextLoader( + mockTypedDataDataSource, + mockTokenDataSource, + ); + + const TEST_TYPES = { + PermitSingle: [ + { + name: "details", + type: "PermitDetails", + }, + { + name: "spender", + type: "address", + }, + { + name: "sigDeadline", + type: "uint256", + }, + ], + PermitDetails: [ + { + type: "address", + name: "token", + }, + { + name: "amount", + type: "uint160", + }, + { + name: "expiration", + type: "uint48", + }, + { + name: "nonce", + type: "uint48", + }, + ], + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + }; + const TEST_VALUES = [ + { + path: "details.token", + value: Uint8Array.from([ + 0x7c, 0xeb, 0x23, 0xfd, 0x6b, 0xc0, 0xad, 0xd5, 0x9e, 0x62, 0xac, 0x25, + 0x57, 0x82, 0x70, 0xcf, 0xf1, 0xb9, 0xf6, 0x19, + ]), + }, + { + path: "details.amount", + value: Uint8Array.from([0x12]), + }, + { + path: "spender", + value: Uint8Array.from([0x12]), + }, + { + path: "details.expiration", + value: Uint8Array.from([0x12]), + }, + ]; + + beforeEach(() => { + jest.restoreAllMocks(); + jest + .spyOn(mockTokenDataSource, "getTokenInfosPayload") + .mockImplementation(({ address }) => + Promise.resolve(Right(`payload-${address}`)), + ); + }); + + describe("load function", () => { + it("success with referenced token", async () => { + // GIVEN + const ctx = { + verifyingContract: "0x000000000022d473030f116ddee9f6b43ac78ba3", + chainId: 1, + version: "v2", + schema: TEST_TYPES, + fieldsValues: TEST_VALUES, + } as TypedDataContext; + jest + .spyOn(mockTypedDataDataSource, "getTypedDataFilters") + .mockImplementation(() => + Promise.resolve( + Right({ + messageInfo: { + displayName: "Permit2", + filtersCount: 4, + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + filters: [ + { + type: "token", + displayName: "Amount allowance", + path: "details.token", + tokenIndex: 0, + signature: + "3044022075103b38995e031d1ebbfe38ac6603bec32854b5146a664e49b4cc4f460c1da6022029f4b0fd1f3b7995ffff1627d4b57f27888a2dcc9b3a4e85c37c67571092c733", + }, + { + type: "amount", + displayName: "Amount allowance", + path: "details.amount", + tokenIndex: 0, + signature: + "304402201a46e6b4ef89eaf9fcf4945d053bfc5616a826400fd758312fbbe976bafc07ec022025a9b408722baf983ee053f90179c75b0c55bb0668f437d55493e36069bbd5a3", + }, + { + type: "raw", + displayName: "Approve to spender", + path: "spender", + signature: + "3044022033e5713d9cb9bc375b56a9fb53b736c81ea3c4ac5cfb2d3ca7f8b8f0558fe2430220543ca4fef6d6f725f29e343f167fe9dd582aa856ecb5797259050eb990a1befb", + }, + { + type: "datetime", + displayName: "Approval expire", + path: "details.expiration", + signature: + "3044022056b3381e4540629ad73bc434ec49d80523234b82f62340fbb77157fb0eb21a680220459fe9cf6ca309f9c7dfc6d4711fea1848dba661563c57f77b3c2dc480b3a63b", + }, + ], + }), + ), + ); + + // WHEN + const result = await loader.load(ctx); + + // THEN + expect(result).toEqual({ + type: "success", + messageInfo: { + displayName: "Permit2", + filtersCount: 4, + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + tokens: { + 0: "payload-0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", + }, + filters: { + "details.amount": { + displayName: "Amount allowance", + path: "details.amount", + signature: + "304402201a46e6b4ef89eaf9fcf4945d053bfc5616a826400fd758312fbbe976bafc07ec022025a9b408722baf983ee053f90179c75b0c55bb0668f437d55493e36069bbd5a3", + tokenIndex: 0, + type: "amount", + }, + "details.expiration": { + displayName: "Approval expire", + path: "details.expiration", + signature: + "3044022056b3381e4540629ad73bc434ec49d80523234b82f62340fbb77157fb0eb21a680220459fe9cf6ca309f9c7dfc6d4711fea1848dba661563c57f77b3c2dc480b3a63b", + type: "datetime", + }, + "details.token": { + displayName: "Amount allowance", + path: "details.token", + signature: + "3044022075103b38995e031d1ebbfe38ac6603bec32854b5146a664e49b4cc4f460c1da6022029f4b0fd1f3b7995ffff1627d4b57f27888a2dcc9b3a4e85c37c67571092c733", + tokenIndex: 0, + type: "token", + }, + spender: { + displayName: "Approve to spender", + path: "spender", + signature: + "3044022033e5713d9cb9bc375b56a9fb53b736c81ea3c4ac5cfb2d3ca7f8b8f0558fe2430220543ca4fef6d6f725f29e343f167fe9dd582aa856ecb5797259050eb990a1befb", + type: "raw", + }, + }, + }); + }); + + it("success with referenced token verifying contract", async () => { + // GIVEN + const ctx = { + verifyingContract: "0x000000000022d473030f116ddee9f6b43ac78ba3", + chainId: 1, + version: "v2", + schema: TEST_TYPES, + fieldsValues: TEST_VALUES, + } as TypedDataContext; + jest + .spyOn(mockTypedDataDataSource, "getTypedDataFilters") + .mockImplementation(() => + Promise.resolve( + Right({ + messageInfo: { + displayName: "Permit2", + filtersCount: 2, + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + filters: [ + { + type: "token", + displayName: "Amount allowance", + path: "details.token", + tokenIndex: 0, + signature: + "3044022075103b38995e031d1ebbfe38ac6603bec32854b5146a664e49b4cc4f460c1da6022029f4b0fd1f3b7995ffff1627d4b57f27888a2dcc9b3a4e85c37c67571092c733", + }, + { + type: "amount", + displayName: "Amount allowance", + path: "details.amount", + tokenIndex: 255, + signature: + "304402201a46e6b4ef89eaf9fcf4945d053bfc5616a826400fd758312fbbe976bafc07ec022025a9b408722baf983ee053f90179c75b0c55bb0668f437d55493e36069bbd5a3", + }, + ], + }), + ), + ); + + // WHEN + const result = await loader.load(ctx); + + // THEN + expect(result).toEqual({ + type: "success", + messageInfo: { + displayName: "Permit2", + filtersCount: 2, + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + tokens: { + 0: "payload-0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", + 255: "payload-0x000000000022d473030f116ddee9f6b43ac78ba3", + }, + filters: { + "details.amount": { + displayName: "Amount allowance", + path: "details.amount", + signature: + "304402201a46e6b4ef89eaf9fcf4945d053bfc5616a826400fd758312fbbe976bafc07ec022025a9b408722baf983ee053f90179c75b0c55bb0668f437d55493e36069bbd5a3", + tokenIndex: 255, + type: "amount", + }, + "details.token": { + displayName: "Amount allowance", + path: "details.token", + signature: + "3044022075103b38995e031d1ebbfe38ac6603bec32854b5146a664e49b4cc4f460c1da6022029f4b0fd1f3b7995ffff1627d4b57f27888a2dcc9b3a4e85c37c67571092c733", + tokenIndex: 0, + type: "token", + }, + }, + }); + }); + + it("should return an error if filters are unavailable", async () => { + // GIVEN + const ctx = { + verifyingContract: "0x000000000022d473030f116ddee9f6b43ac78ba3", + chainId: 1, + version: "v2", + schema: TEST_TYPES, + fieldsValues: TEST_VALUES, + } as TypedDataContext; + jest + .spyOn(mockTypedDataDataSource, "getTypedDataFilters") + .mockImplementation(() => Promise.resolve(Left(new Error("error")))); + + // WHEN + const result = await loader.load(ctx); + + // THEN + expect(result).toEqual({ + type: "error", + error: new Error("error"), + }); + }); + + it("should return an error if tokens are unavailable", async () => { + // GIVEN + const ctx = { + verifyingContract: "0x000000000022d473030f116ddee9f6b43ac78ba3", + chainId: 1, + version: "v2", + schema: TEST_TYPES, + fieldsValues: TEST_VALUES, + } as TypedDataContext; + jest + .spyOn(mockTypedDataDataSource, "getTypedDataFilters") + .mockImplementation(() => + Promise.resolve( + Right({ + messageInfo: { + displayName: "Permit2", + filtersCount: 2, + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + filters: [ + { + type: "token", + displayName: "Amount allowance", + path: "details.token", + tokenIndex: 0, + signature: + "3044022075103b38995e031d1ebbfe38ac6603bec32854b5146a664e49b4cc4f460c1da6022029f4b0fd1f3b7995ffff1627d4b57f27888a2dcc9b3a4e85c37c67571092c733", + }, + ], + }), + ), + ); + jest + .spyOn(mockTokenDataSource, "getTokenInfosPayload") + .mockImplementation(() => + Promise.resolve(Left(new Error("token error"))), + ); + + // WHEN + const result = await loader.load(ctx); + + // THEN + expect(result).toEqual({ + type: "error", + error: new Error("token error"), + }); + }); + + it("should return an error if value is not found", async () => { + // GIVEN + const ctx = { + verifyingContract: "0x000000000022d473030f116ddee9f6b43ac78ba3", + chainId: 1, + version: "v2", + schema: TEST_TYPES, + fieldsValues: TEST_VALUES, + } as TypedDataContext; + jest + .spyOn(mockTypedDataDataSource, "getTypedDataFilters") + .mockImplementation(() => + Promise.resolve( + Right({ + messageInfo: { + displayName: "Permit2", + filtersCount: 2, + signature: + "3045022100e3c597d13d28a87a88b0239404c668373cf5063362f2a81d09eed4582941dfe802207669aabb504fd5b95b2734057f6b8bbf51f14a69a5f9bdf658a5952cefbf44d3", + }, + filters: [ + { + type: "token", + displayName: "Amount allowance", + path: "details.badtoken", + tokenIndex: 0, + signature: + "3044022075103b38995e031d1ebbfe38ac6603bec32854b5146a664e49b4cc4f460c1da6022029f4b0fd1f3b7995ffff1627d4b57f27888a2dcc9b3a4e85c37c67571092c733", + }, + ], + }), + ), + ); + + // WHEN + const result = await loader.load(ctx); + + // THEN + expect(result).toEqual({ + type: "error", + error: new Error( + "The token filter references the value details.badtoken which is absent from the message", + ), + }); + }); + }); +}); diff --git a/packages/signer/context-module/src/typed-data/domain/DefaultTypedDataContextLoader.ts b/packages/signer/context-module/src/typed-data/domain/DefaultTypedDataContextLoader.ts new file mode 100644 index 000000000..6629848ad --- /dev/null +++ b/packages/signer/context-module/src/typed-data/domain/DefaultTypedDataContextLoader.ts @@ -0,0 +1,136 @@ +import type { HexaString } from "@ledgerhq/device-sdk-core"; +import { inject, injectable } from "inversify"; + +import type { + TypedDataClearSignContext, + TypedDataFilter, + TypedDataFilterPath, + TypedDataToken, + TypedDataTokenIndex, +} from "@/shared/model/TypedDataClearSignContext"; +import { VERIFYING_CONTRACT_TOKEN_INDEX } from "@/shared/model/TypedDataClearSignContext"; +import type { TypedDataContext } from "@/shared/model/TypedDataContext"; +import type { TokenDataSource } from "@/token/data/TokenDataSource"; +import { tokenTypes } from "@/token/di/tokenTypes"; +import type { TypedDataDataSource } from "@/typed-data/data/TypedDataDataSource"; +import { typedDataTypes } from "@/typed-data/di/typedDataTypes"; +import type { TypedDataContextLoader } from "@/typed-data/domain/TypedDataContextLoader"; + +@injectable() +export class DefaultTypedDataContextLoader implements TypedDataContextLoader { + constructor( + @inject(typedDataTypes.TypedDataDataSource) + private dataSource: TypedDataDataSource, + @inject(tokenTypes.TokenDataSource) + private tokenDataSource: TokenDataSource, + ) {} + + async load(typedData: TypedDataContext): Promise { + // Get the typed data filters from the data source + const data = await this.dataSource.getTypedDataFilters({ + address: typedData.verifyingContract, + chainId: typedData.chainId, + version: typedData.version, + schema: typedData.schema, + }); + + // If there was an error getting the typed data filters, return an error immediately + if (data.isLeft()) { + return { + type: "error", + error: data.extract(), + }; + } + + // Else, extract the message info and filters + const { messageInfo, filters } = data.unsafeCoerce(); + + // Loop through the typed data filters to extract informations + const mappedFilters: Record = {}; + const mappedTokens: Record = {}; + for (const filter of filters) { + // Add the filter to the clear signing context + mappedFilters[filter.path] = filter; + if (filter.type !== "token" && filter.type !== "amount") { + continue; // no token reference + } + + // If the filter references a token, retrieve its descriptor from the tokens data source + const tokenIndex = filter.tokenIndex; + if (mappedTokens[tokenIndex] !== undefined) { + continue; // Already fetched for a previous filter + } + + // If the filter is a token, get token address from typed message values, and fetch descriptor + if (filter.type === "token") { + const value = typedData.fieldsValues.find( + (entry) => entry.path === filter.path, + ); + if (value === undefined) { + return { + type: "error", + error: new Error( + `The token filter references the value ${filter.path} which is absent from the message`, + ), + }; + } + // Fetch descriptor + const address = this.convertAddressToHexaString(value.value); + const chainId = typedData.chainId; + const payload = await this.tokenDataSource.getTokenInfosPayload({ + address, + chainId, + }); + if (payload.isLeft()) { + return { + type: "error", + error: payload.extract(), + }; + } + payload.ifRight((payload) => { + mappedTokens[tokenIndex] = payload; + }); + } + + // If the filter is an amount with a reference to the verifyingContract, fetch verifyingContract descriptor. + // This is because descriptors data-sources should be compatible with Ledger devices specifications: + // https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#amount-join-value + else if ( + filter.type === "amount" && + tokenIndex === VERIFYING_CONTRACT_TOKEN_INDEX + ) { + const address = typedData.verifyingContract; + const chainId = typedData.chainId; + const payload = await this.tokenDataSource.getTokenInfosPayload({ + address, + chainId, + }); + if (payload.isLeft()) { + return { + type: "error", + error: payload.extract(), + }; + } + payload.ifRight((payload) => { + mappedTokens[tokenIndex] = payload; + }); + } + } + + return { + type: "success", + messageInfo, + filters: mappedFilters, + tokens: mappedTokens, + }; + } + + private convertAddressToHexaString(address: Uint8Array): HexaString { + // Address size is 20 bytes so 40 characters, padded with zeros on the left + return `0x${Array.from(address, (byte) => + byte.toString(16).padStart(2, "0"), + ) + .join("") + .padStart(40, "0")}`; + } +} diff --git a/packages/signer/context-module/src/typed-data/domain/TypedDataContextLoader.ts b/packages/signer/context-module/src/typed-data/domain/TypedDataContextLoader.ts new file mode 100644 index 000000000..a75e84362 --- /dev/null +++ b/packages/signer/context-module/src/typed-data/domain/TypedDataContextLoader.ts @@ -0,0 +1,6 @@ +import type { TypedDataClearSignContext } from "@/shared/model/TypedDataClearSignContext"; +import type { TypedDataContext } from "@/shared/model/TypedDataContext"; + +export interface TypedDataContextLoader { + load(typedData: TypedDataContext): Promise; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c31c2ad37..1c8cfbe6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,9 @@ importers: axios: specifier: ^1.7.2 version: 1.7.2 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 ethers: specifier: ^6.13.2 version: 6.13.2 @@ -251,6 +254,9 @@ importers: '@ledgerhq/tsconfig-dsdk': specifier: workspace:* version: link:../../config/typescript + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.4.0)(typescript@5.5.3) @@ -2318,6 +2324,9 @@ packages: '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -3126,6 +3135,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + crypto-random-string@4.0.0: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} @@ -9482,6 +9494,8 @@ snapshots: dependencies: '@types/node': 20.14.11 + '@types/crypto-js@4.2.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 8.56.10 @@ -10462,6 +10476,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + crypto-random-string@4.0.0: dependencies: type-fest: 1.4.0