@ledgerhq/coin-solana 0.56.0 → 0.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ledgerhq/coin-solana
2
2
 
3
+ ## 0.56.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#19512](https://github.com/LedgerHQ/ledger-live/pull/19512) [`7c0729b`](https://github.com/LedgerHQ/ledger-live/commit/7c0729b140c2d1e7bfb929eac701a4c6bba2f9a9) Thanks [@fAnselmi-Ledger](https://github.com/fAnselmi-Ledger)! - Add new OCMS v1 msg format
8
+
9
+ ## 0.56.1-hotfix.0
10
+
11
+ ### Patch Changes
12
+
13
+ - [#19512](https://github.com/LedgerHQ/ledger-live/pull/19512) [`7c0729b`](https://github.com/LedgerHQ/ledger-live/commit/7c0729b140c2d1e7bfb929eac701a4c6bba2f9a9) Thanks [@fAnselmi-Ledger](https://github.com/fAnselmi-Ledger)! - Add new OCMS v1 msg format
14
+
3
15
  ## 0.56.0
4
16
 
5
17
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"hw-signMessage.d.ts","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAMrE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,eAAO,MAAM,WAAW,GACrB,eAAe,aAAa,CAAC,YAAY,CAAC,MAEzC,UAAU,QAAQ,EAClB,SAAS,OAAO,EAChB,gBAAgB,UAAU,KACzB,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;CACnB,CA2BA,CAAC"}
1
+ {"version":3,"file":"hw-signMessage.d.ts","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAOrE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAkBxC,eAAO,MAAM,WAAW,GACrB,eAAe,aAAa,CAAC,YAAY,CAAC,MAEzC,UAAU,QAAQ,EAClB,SAAS,OAAO,EAChB,gBAAgB,UAAU,KACzB,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;CACnB,CA4DA,CAAC"}
@@ -4,11 +4,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.signMessage = void 0;
7
+ const errors_1 = require("@ledgerhq/errors");
7
8
  const bs58_1 = __importDefault(require("bs58"));
8
9
  const invariant_1 = __importDefault(require("invariant"));
9
10
  const semver_1 = __importDefault(require("semver"));
10
11
  const config_1 = __importDefault(require("./config"));
11
12
  const format_1 = require("./offchainMessage/format");
13
+ // Returns true for device rejections that should trigger a format fallback
14
+ // (e.g. firmware does not support this OCM layout). Propagates user refusals
15
+ // and locked-device errors so they are never silently retried.
16
+ // Handles both DmkSignerSol (maps to typed errors) and LegacySignerSolana
17
+ // (propagates raw TransportStatusError with a numeric statusCode).
18
+ function isFormatRejection(err) {
19
+ if (err instanceof errors_1.UserRefusedOnDevice)
20
+ return false;
21
+ if (err instanceof errors_1.LockedDeviceError)
22
+ return false;
23
+ // TransportStatusError duck-type: 0x6985 = user refused, 0x5515 = device locked
24
+ if (err && typeof err === "object" && "statusCode" in err) {
25
+ const { statusCode } = err;
26
+ if (statusCode === 0x6985 || statusCode === 0x5515)
27
+ return false;
28
+ }
29
+ return true;
30
+ }
12
31
  const signMessage = (signerContext) => async (deviceId, account, messageOptions) => {
13
32
  const message = messageOptions.message;
14
33
  if (!message || typeof message !== "string") {
@@ -18,8 +37,37 @@ const signMessage = (signerContext) => async (deviceId, account, messageOptions)
18
37
  const result = await signerContext(deviceId, async (signer) => {
19
38
  const { version } = await signer.getAppConfiguration();
20
39
  const isLegacy = semver_1.default.lt(version, config_1.default.getCoinConfig().legacyOCMSMaxVersion);
21
- signedMessage = (0, format_1.toOffChainMessage)(message, account.freshAddress, isLegacy);
22
- return signer.signMessage(account.freshAddressPath, signedMessage.toString("hex"));
40
+ if (isLegacy) {
41
+ const ocm = (0, format_1.toOffChainMessage)(message, account.freshAddress, true);
42
+ signedMessage = ocm;
43
+ return signer.signMessage(account.freshAddressPath, ocm.toString("hex"));
44
+ }
45
+ // Step 1 — V1 without length prefix (finalised sRFC 38 spec).
46
+ const v1NoLength = (0, format_1.toOffChainMessageV1)(message, account.freshAddress, false);
47
+ try {
48
+ const sig = await signer.signMessage(account.freshAddressPath, v1NoLength.toString("hex"));
49
+ signedMessage = v1NoLength;
50
+ return sig;
51
+ }
52
+ catch (err) {
53
+ if (!isFormatRejection(err))
54
+ throw err;
55
+ }
56
+ // Step 2 — V1 with length prefix (pre-sRFC-38 firmware).
57
+ const v1WithLength = (0, format_1.toOffChainMessageV1)(message, account.freshAddress, true);
58
+ try {
59
+ const sig = await signer.signMessage(account.freshAddressPath, v1WithLength.toString("hex"));
60
+ signedMessage = v1WithLength;
61
+ return sig;
62
+ }
63
+ catch (err) {
64
+ if (!isFormatRejection(err))
65
+ throw err;
66
+ }
67
+ // Step 3 — V0 fallback (older firmware that does not support V1 at all).
68
+ const v0 = (0, format_1.toOffChainMessage)(message, account.freshAddress, false);
69
+ signedMessage = v0;
70
+ return signer.signMessage(account.freshAddressPath, v0.toString("hex"));
23
71
  });
24
72
  (0, invariant_1.default)(signedMessage, "signedMessage should exist");
25
73
  const signatureCount = Buffer.from([1]);
@@ -1 +1 @@
1
- {"version":3,"file":"hw-signMessage.js","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":";;;;;;AAEA,gDAAwB;AACxB,0DAAkC;AAClC,oDAA4B;AAC5B,sDAAkC;AAClC,qDAA6D;AAGtD,MAAM,WAAW,GACtB,CAAC,aAA0C,EAAE,EAAE,CAC/C,KAAK,EACH,QAAkB,EAClB,OAAgB,EAChB,cAA0B,EAGzB,EAAE;IACH,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC;IACvC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,6EAA6E,CAC9E,CAAC;IACJ,CAAC;IAED,IAAI,aAAiC,CAAC;IAEtC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,KAAK,EAAC,MAAM,EAAC,EAAE;QAC1D,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,gBAAM,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAU,CAAC,aAAa,EAAE,CAAC,oBAAoB,CAAC,CAAC;QAErF,aAAa,GAAG,IAAA,0BAAiB,EAAC,OAAO,EAAE,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QAE3E,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,gBAAgB,EAAE,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH,IAAA,mBAAS,EAAC,aAAa,EAAE,4BAA4B,CAAC,CAAC;IAEvD,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAExC,qEAAqE;IACrE,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,MAAM,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;IAElF,OAAO,EAAE,SAAS,EAAE,cAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC9C,CAAC,CAAC;AAnCS,QAAA,WAAW,eAmCpB"}
1
+ {"version":3,"file":"hw-signMessage.js","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"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"}
@@ -3,5 +3,19 @@
3
3
  * Thus, have not been fully tested (unit tests + manual tests)
4
4
  * We should add more tests in the future
5
5
  */
6
+ /**
7
+ * V1 wire format (sRFC 38, per app-solana parser):
8
+ * signing domain (16 B): 0xFF + "solana offchain"
9
+ * version ( 1 B): 0x01
10
+ * signer count ( 1 B): 1
11
+ * signer (32 B)
12
+ * [message length ( 2 B): little-endian uint16] — only when includeLengthPrefix
13
+ * message body (variable)
14
+ *
15
+ * The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
16
+ * reproduces the pre-spec-update layout for backward compatibility with older
17
+ * firmware that rejects the no-prefix form with 6a81.
18
+ */
19
+ export declare function toOffChainMessageV1(message: string, signerAddress: string, includeLengthPrefix?: boolean): Buffer;
6
20
  export declare function toOffChainMessage(message: string, signerAddress: string, isLegacy: boolean): Buffer;
7
21
  //# sourceMappingURL=format.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkEH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,OAAO,GAChB,MAAM,CAuBR"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2CH;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,mBAAmB,UAAQ,GAC1B,MAAM,CAkBR;AA0BD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,OAAO,GAChB,MAAM,CA6BR"}
@@ -5,13 +5,12 @@
5
5
  * We should add more tests in the future
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.toOffChainMessageV1 = toOffChainMessageV1;
8
9
  exports.toOffChainMessage = toOffChainMessage;
9
10
  const web3_js_1 = require("@solana/web3.js");
10
- // Max off-chain message length supported by Ledger
11
+ // Max off-chain message length supported by Ledger (= app-solana MAX_OFFCHAIN_MESSAGE_LENGTH)
11
12
  const OFFCM_MAX_LEDGER_LEN = 15 * 1024 - 40 - 8;
12
13
  const LEGACY_OFFCM_MAX_LEDGER_LEN = 1280 - 40 - 8;
13
- // Max length of version 0 off-chain message
14
- const OFFCM_MAX_V0_LEN = 65515;
15
14
  const MAX_PRINTABLE_ASCII = 0x7e;
16
15
  const MIN_PRINTABLE_ASCII = 0x20;
17
16
  const LINE_FEED_ASCII = 0x0a;
@@ -35,32 +34,63 @@ function isUTF8(buffer) {
35
34
  }
36
35
  function findMessageFormat(messageBuffer, isLegacy) {
37
36
  const maxLedgerLen = isLegacy ? LEGACY_OFFCM_MAX_LEDGER_LEN : OFFCM_MAX_LEDGER_LEN;
38
- if (messageBuffer.length <= maxLedgerLen) {
39
- if (isPrintableASCII(messageBuffer, isLegacy)) {
40
- return 0;
41
- }
42
- else if (isUTF8(messageBuffer)) {
43
- return 1;
44
- }
45
- }
46
- else if (messageBuffer.length <= OFFCM_MAX_V0_LEN) {
47
- if (isUTF8(messageBuffer)) {
48
- return 2;
49
- }
37
+ if (messageBuffer.length > maxLedgerLen) {
38
+ throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${maxLedgerLen})`);
50
39
  }
40
+ if (isPrintableASCII(messageBuffer, isLegacy))
41
+ return 0;
42
+ if (isUTF8(messageBuffer))
43
+ return 1;
51
44
  return 0;
52
45
  }
46
+ const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
53
47
  /**
54
- * 1. Signing domain (16 bytes)
55
- * 2. Header version (1 byte)
56
- * 3. Application domain (32 bytes)
57
- * 4. Message format (1 byte)
58
- * 5. Signer count (1 bytes)
59
- * 6. Signers (signer_count * 32 bytes) - assume that only one signer is present
60
- * 7. Message length (2 bytes)
48
+ * V1 wire format (sRFC 38, per app-solana parser):
49
+ * signing domain (16 B): 0xFF + "solana offchain"
50
+ * version ( 1 B): 0x01
51
+ * signer count ( 1 B): 1
52
+ * signer (32 B)
53
+ * [message length ( 2 B): little-endian uint16] only when includeLengthPrefix
54
+ * message body (variable)
55
+ *
56
+ * The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
57
+ * reproduces the pre-spec-update layout for backward compatibility with older
58
+ * firmware that rejects the no-prefix form with 6a81.
61
59
  */
62
- const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
63
- const headerVersion = Buffer.alloc(1);
60
+ function toOffChainMessageV1(message, signerAddress, includeLengthPrefix = false) {
61
+ const messageBuffer = Buffer.from(message);
62
+ if (messageBuffer.length > OFFCM_MAX_LEDGER_LEN) {
63
+ throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${OFFCM_MAX_LEDGER_LEN})`);
64
+ }
65
+ const version = Buffer.from([0x01]);
66
+ const signerCount = Buffer.from([0x01]);
67
+ const signer = new web3_js_1.PublicKey(signerAddress).toBuffer();
68
+ if (includeLengthPrefix) {
69
+ const messageLength = Buffer.alloc(2);
70
+ messageLength.writeUInt16LE(messageBuffer.length);
71
+ return Buffer.concat([signingDomain, version, signerCount, signer, messageLength, messageBuffer]);
72
+ }
73
+ return Buffer.concat([signingDomain, version, signerCount, signer, messageBuffer]);
74
+ }
75
+ /**
76
+ * V0 wire format:
77
+ * signing domain (16 B): 0xFF + "solana offchain"
78
+ * version ( 1 B): 0x00
79
+ * application domain (32 B): zero-padded
80
+ * format ( 1 B): 0 = ASCII, 1 = UTF-8
81
+ * signer count ( 1 B): 1
82
+ * signer (32 B)
83
+ * message length ( 2 B): little-endian uint16
84
+ * message body (variable)
85
+ *
86
+ * Legacy format (Nano S / old firmware):
87
+ * signing domain (16 B)
88
+ * version ( 1 B): 0x00
89
+ * format ( 1 B)
90
+ * message length ( 2 B): always present
91
+ * message body (variable)
92
+ */
93
+ const v0HeaderVersion = Buffer.alloc(1); // 0x00
64
94
  const applicationDomain = Buffer.alloc(32);
65
95
  const messageFormat = Buffer.alloc(1);
66
96
  const signerCount = Buffer.alloc(1);
@@ -70,18 +100,25 @@ function toOffChainMessage(message, signerAddress, isLegacy) {
70
100
  const messageBuffer = Buffer.from(message);
71
101
  messageFormat.writeUInt8(findMessageFormat(messageBuffer, isLegacy));
72
102
  const signers = new web3_js_1.PublicKey(signerAddress).toBuffer();
73
- messageLength.writeUint16LE(messageBuffer.length);
74
- return Buffer.concat(isLegacy
75
- ? [signingDomain, headerVersion, messageFormat, messageLength, messageBuffer]
76
- : [
103
+ messageLength.writeUInt16LE(messageBuffer.length);
104
+ if (isLegacy) {
105
+ return Buffer.concat([
77
106
  signingDomain,
78
- headerVersion,
79
- applicationDomain,
107
+ v0HeaderVersion,
80
108
  messageFormat,
81
- signerCount,
82
- signers,
83
109
  messageLength,
84
110
  messageBuffer,
85
111
  ]);
112
+ }
113
+ return Buffer.concat([
114
+ signingDomain,
115
+ v0HeaderVersion,
116
+ applicationDomain,
117
+ messageFormat,
118
+ signerCount,
119
+ signers,
120
+ messageLength,
121
+ messageBuffer,
122
+ ]);
86
123
  }
87
124
  //# sourceMappingURL=format.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"hw-signMessage.d.ts","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAMrE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,eAAO,MAAM,WAAW,GACrB,eAAe,aAAa,CAAC,YAAY,CAAC,MAEzC,UAAU,QAAQ,EAClB,SAAS,OAAO,EAChB,gBAAgB,UAAU,KACzB,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;CACnB,CA2BA,CAAC"}
1
+ {"version":3,"file":"hw-signMessage.d.ts","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAOrE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAkBxC,eAAO,MAAM,WAAW,GACrB,eAAe,aAAa,CAAC,YAAY,CAAC,MAEzC,UAAU,QAAQ,EAClB,SAAS,OAAO,EAChB,gBAAgB,UAAU,KACzB,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;CACnB,CA4DA,CAAC"}
@@ -1,8 +1,27 @@
1
+ import { LockedDeviceError, UserRefusedOnDevice } from "@ledgerhq/errors";
1
2
  import bs58 from "bs58";
2
3
  import invariant from "invariant";
3
4
  import semver from "semver";
4
5
  import coinConfig from "./config";
5
- import { toOffChainMessage } from "./offchainMessage/format";
6
+ import { toOffChainMessage, toOffChainMessageV1 } from "./offchainMessage/format";
7
+ // Returns true for device rejections that should trigger a format fallback
8
+ // (e.g. firmware does not support this OCM layout). Propagates user refusals
9
+ // and locked-device errors so they are never silently retried.
10
+ // Handles both DmkSignerSol (maps to typed errors) and LegacySignerSolana
11
+ // (propagates raw TransportStatusError with a numeric statusCode).
12
+ function isFormatRejection(err) {
13
+ if (err instanceof UserRefusedOnDevice)
14
+ return false;
15
+ if (err instanceof LockedDeviceError)
16
+ return false;
17
+ // TransportStatusError duck-type: 0x6985 = user refused, 0x5515 = device locked
18
+ if (err && typeof err === "object" && "statusCode" in err) {
19
+ const { statusCode } = err;
20
+ if (statusCode === 0x6985 || statusCode === 0x5515)
21
+ return false;
22
+ }
23
+ return true;
24
+ }
6
25
  export const signMessage = (signerContext) => async (deviceId, account, messageOptions) => {
7
26
  const message = messageOptions.message;
8
27
  if (!message || typeof message !== "string") {
@@ -12,8 +31,37 @@ export const signMessage = (signerContext) => async (deviceId, account, messageO
12
31
  const result = await signerContext(deviceId, async (signer) => {
13
32
  const { version } = await signer.getAppConfiguration();
14
33
  const isLegacy = semver.lt(version, coinConfig.getCoinConfig().legacyOCMSMaxVersion);
15
- signedMessage = toOffChainMessage(message, account.freshAddress, isLegacy);
16
- return signer.signMessage(account.freshAddressPath, signedMessage.toString("hex"));
34
+ if (isLegacy) {
35
+ const ocm = toOffChainMessage(message, account.freshAddress, true);
36
+ signedMessage = ocm;
37
+ return signer.signMessage(account.freshAddressPath, ocm.toString("hex"));
38
+ }
39
+ // Step 1 — V1 without length prefix (finalised sRFC 38 spec).
40
+ const v1NoLength = toOffChainMessageV1(message, account.freshAddress, false);
41
+ try {
42
+ const sig = await signer.signMessage(account.freshAddressPath, v1NoLength.toString("hex"));
43
+ signedMessage = v1NoLength;
44
+ return sig;
45
+ }
46
+ catch (err) {
47
+ if (!isFormatRejection(err))
48
+ throw err;
49
+ }
50
+ // Step 2 — V1 with length prefix (pre-sRFC-38 firmware).
51
+ const v1WithLength = toOffChainMessageV1(message, account.freshAddress, true);
52
+ try {
53
+ const sig = await signer.signMessage(account.freshAddressPath, v1WithLength.toString("hex"));
54
+ signedMessage = v1WithLength;
55
+ return sig;
56
+ }
57
+ catch (err) {
58
+ if (!isFormatRejection(err))
59
+ throw err;
60
+ }
61
+ // Step 3 — V0 fallback (older firmware that does not support V1 at all).
62
+ const v0 = toOffChainMessage(message, account.freshAddress, false);
63
+ signedMessage = v0;
64
+ return signer.signMessage(account.freshAddressPath, v0.toString("hex"));
17
65
  });
18
66
  invariant(signedMessage, "signedMessage should exist");
19
67
  const signatureCount = Buffer.from([1]);
@@ -1 +1 @@
1
- {"version":3,"file":"hw-signMessage.js","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAEA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,SAAS,MAAM,WAAW,CAAC;AAClC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,UAAU,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG7D,MAAM,CAAC,MAAM,WAAW,GACtB,CAAC,aAA0C,EAAE,EAAE,CAC/C,KAAK,EACH,QAAkB,EAClB,OAAgB,EAChB,cAA0B,EAGzB,EAAE;IACH,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC;IACvC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,6EAA6E,CAC9E,CAAC;IACJ,CAAC;IAED,IAAI,aAAiC,CAAC;IAEtC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,KAAK,EAAC,MAAM,EAAC,EAAE;QAC1D,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,aAAa,EAAE,CAAC,oBAAoB,CAAC,CAAC;QAErF,aAAa,GAAG,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QAE3E,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,gBAAgB,EAAE,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,aAAa,EAAE,4BAA4B,CAAC,CAAC;IAEvD,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAExC,qEAAqE;IACrE,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,MAAM,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;IAElF,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC9C,CAAC,CAAC"}
1
+ {"version":3,"file":"hw-signMessage.js","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"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"}
@@ -3,5 +3,19 @@
3
3
  * Thus, have not been fully tested (unit tests + manual tests)
4
4
  * We should add more tests in the future
5
5
  */
6
+ /**
7
+ * V1 wire format (sRFC 38, per app-solana parser):
8
+ * signing domain (16 B): 0xFF + "solana offchain"
9
+ * version ( 1 B): 0x01
10
+ * signer count ( 1 B): 1
11
+ * signer (32 B)
12
+ * [message length ( 2 B): little-endian uint16] — only when includeLengthPrefix
13
+ * message body (variable)
14
+ *
15
+ * The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
16
+ * reproduces the pre-spec-update layout for backward compatibility with older
17
+ * firmware that rejects the no-prefix form with 6a81.
18
+ */
19
+ export declare function toOffChainMessageV1(message: string, signerAddress: string, includeLengthPrefix?: boolean): Buffer;
6
20
  export declare function toOffChainMessage(message: string, signerAddress: string, isLegacy: boolean): Buffer;
7
21
  //# sourceMappingURL=format.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkEH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,OAAO,GAChB,MAAM,CAuBR"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2CH;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,mBAAmB,UAAQ,GAC1B,MAAM,CAkBR;AA0BD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,OAAO,GAChB,MAAM,CA6BR"}
@@ -4,11 +4,9 @@
4
4
  * We should add more tests in the future
5
5
  */
6
6
  import { PublicKey } from "@solana/web3.js";
7
- // Max off-chain message length supported by Ledger
7
+ // Max off-chain message length supported by Ledger (= app-solana MAX_OFFCHAIN_MESSAGE_LENGTH)
8
8
  const OFFCM_MAX_LEDGER_LEN = 15 * 1024 - 40 - 8;
9
9
  const LEGACY_OFFCM_MAX_LEDGER_LEN = 1280 - 40 - 8;
10
- // Max length of version 0 off-chain message
11
- const OFFCM_MAX_V0_LEN = 65515;
12
10
  const MAX_PRINTABLE_ASCII = 0x7e;
13
11
  const MIN_PRINTABLE_ASCII = 0x20;
14
12
  const LINE_FEED_ASCII = 0x0a;
@@ -32,32 +30,63 @@ function isUTF8(buffer) {
32
30
  }
33
31
  function findMessageFormat(messageBuffer, isLegacy) {
34
32
  const maxLedgerLen = isLegacy ? LEGACY_OFFCM_MAX_LEDGER_LEN : OFFCM_MAX_LEDGER_LEN;
35
- if (messageBuffer.length <= maxLedgerLen) {
36
- if (isPrintableASCII(messageBuffer, isLegacy)) {
37
- return 0;
38
- }
39
- else if (isUTF8(messageBuffer)) {
40
- return 1;
41
- }
42
- }
43
- else if (messageBuffer.length <= OFFCM_MAX_V0_LEN) {
44
- if (isUTF8(messageBuffer)) {
45
- return 2;
46
- }
33
+ if (messageBuffer.length > maxLedgerLen) {
34
+ throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${maxLedgerLen})`);
47
35
  }
36
+ if (isPrintableASCII(messageBuffer, isLegacy))
37
+ return 0;
38
+ if (isUTF8(messageBuffer))
39
+ return 1;
48
40
  return 0;
49
41
  }
42
+ const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
50
43
  /**
51
- * 1. Signing domain (16 bytes)
52
- * 2. Header version (1 byte)
53
- * 3. Application domain (32 bytes)
54
- * 4. Message format (1 byte)
55
- * 5. Signer count (1 bytes)
56
- * 6. Signers (signer_count * 32 bytes) - assume that only one signer is present
57
- * 7. Message length (2 bytes)
44
+ * V1 wire format (sRFC 38, per app-solana parser):
45
+ * signing domain (16 B): 0xFF + "solana offchain"
46
+ * version ( 1 B): 0x01
47
+ * signer count ( 1 B): 1
48
+ * signer (32 B)
49
+ * [message length ( 2 B): little-endian uint16] only when includeLengthPrefix
50
+ * message body (variable)
51
+ *
52
+ * The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
53
+ * reproduces the pre-spec-update layout for backward compatibility with older
54
+ * firmware that rejects the no-prefix form with 6a81.
58
55
  */
59
- const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
60
- const headerVersion = Buffer.alloc(1);
56
+ export function toOffChainMessageV1(message, signerAddress, includeLengthPrefix = false) {
57
+ const messageBuffer = Buffer.from(message);
58
+ if (messageBuffer.length > OFFCM_MAX_LEDGER_LEN) {
59
+ throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${OFFCM_MAX_LEDGER_LEN})`);
60
+ }
61
+ const version = Buffer.from([0x01]);
62
+ const signerCount = Buffer.from([0x01]);
63
+ const signer = new PublicKey(signerAddress).toBuffer();
64
+ if (includeLengthPrefix) {
65
+ const messageLength = Buffer.alloc(2);
66
+ messageLength.writeUInt16LE(messageBuffer.length);
67
+ return Buffer.concat([signingDomain, version, signerCount, signer, messageLength, messageBuffer]);
68
+ }
69
+ return Buffer.concat([signingDomain, version, signerCount, signer, messageBuffer]);
70
+ }
71
+ /**
72
+ * V0 wire format:
73
+ * signing domain (16 B): 0xFF + "solana offchain"
74
+ * version ( 1 B): 0x00
75
+ * application domain (32 B): zero-padded
76
+ * format ( 1 B): 0 = ASCII, 1 = UTF-8
77
+ * signer count ( 1 B): 1
78
+ * signer (32 B)
79
+ * message length ( 2 B): little-endian uint16
80
+ * message body (variable)
81
+ *
82
+ * Legacy format (Nano S / old firmware):
83
+ * signing domain (16 B)
84
+ * version ( 1 B): 0x00
85
+ * format ( 1 B)
86
+ * message length ( 2 B): always present
87
+ * message body (variable)
88
+ */
89
+ const v0HeaderVersion = Buffer.alloc(1); // 0x00
61
90
  const applicationDomain = Buffer.alloc(32);
62
91
  const messageFormat = Buffer.alloc(1);
63
92
  const signerCount = Buffer.alloc(1);
@@ -67,18 +96,25 @@ export function toOffChainMessage(message, signerAddress, isLegacy) {
67
96
  const messageBuffer = Buffer.from(message);
68
97
  messageFormat.writeUInt8(findMessageFormat(messageBuffer, isLegacy));
69
98
  const signers = new PublicKey(signerAddress).toBuffer();
70
- messageLength.writeUint16LE(messageBuffer.length);
71
- return Buffer.concat(isLegacy
72
- ? [signingDomain, headerVersion, messageFormat, messageLength, messageBuffer]
73
- : [
99
+ messageLength.writeUInt16LE(messageBuffer.length);
100
+ if (isLegacy) {
101
+ return Buffer.concat([
74
102
  signingDomain,
75
- headerVersion,
76
- applicationDomain,
103
+ v0HeaderVersion,
77
104
  messageFormat,
78
- signerCount,
79
- signers,
80
105
  messageLength,
81
106
  messageBuffer,
82
107
  ]);
108
+ }
109
+ return Buffer.concat([
110
+ signingDomain,
111
+ v0HeaderVersion,
112
+ applicationDomain,
113
+ messageFormat,
114
+ signerCount,
115
+ signers,
116
+ messageLength,
117
+ messageBuffer,
118
+ ]);
83
119
  }
84
120
  //# sourceMappingURL=format.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,8FAA8F;AAC9F,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC;AAChD,MAAM,2BAA2B,GAAG,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC;AAElD,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACjC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACjC,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,SAAS,gBAAgB,CAAC,MAAc,EAAE,QAAiB;IACzD,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE;QAC5B,OAAO;QACL,qDAAqD;QACrD,iEAAiE;QACjE,CAAC,CAAC,QAAQ,IAAI,OAAO,KAAK,eAAe,CAAC;YAC1C,CAAC,mBAAmB,IAAI,OAAO,IAAI,OAAO,IAAI,mBAAmB,CAAC,CACnE,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,MAAM,CAAC,MAAc;IAC5B,IAAI,CAAC;QACH,IAAI,WAAW,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,aAAqB,EAAE,QAAiB;IACjE,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,oBAAoB,CAAC;IACnF,IAAI,aAAa,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,qBAAqB,aAAa,CAAC,MAAM,kBAAkB,YAAY,GAAG,CAAC,CAAC;IAC9F,CAAC;IACD,IAAI,gBAAgB,CAAC,aAAa,EAAE,QAAQ,CAAC;QAAE,OAAO,CAAC,CAAC;IACxD,IAAI,MAAM,CAAC,aAAa,CAAC;QAAE,OAAO,CAAC,CAAC;IACpC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;AAE1F;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAe,EACf,aAAqB,EACrB,mBAAmB,GAAG,KAAK;IAE3B,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,aAAa,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CACb,qBAAqB,aAAa,CAAC,MAAM,kBAAkB,oBAAoB,GAAG,CACnF,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,aAAa,CAAC,CAAC,QAAQ,EAAE,CAAC;IAEvD,IAAI,mBAAmB,EAAE,CAAC;QACxB,MAAM,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACtC,aAAa,CAAC,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAClD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;AAChD,MAAM,iBAAiB,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAC3C,MAAM,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACtC,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AAC1B,MAAM,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACtC,MAAM,UAAU,iBAAiB,CAC/B,OAAe,EACf,aAAqB,EACrB,QAAiB;IAEjB,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE3C,aAAa,CAAC,UAAU,CAAC,iBAAiB,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC,CAAC;IAErE,MAAM,OAAO,GAAG,IAAI,SAAS,CAAC,aAAa,CAAC,CAAC,QAAQ,EAAE,CAAC;IAExD,aAAa,CAAC,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAElD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,MAAM,CAAC,MAAM,CAAC;YACnB,aAAa;YACb,eAAe;YACf,aAAa;YACb,aAAa;YACb,aAAa;SACd,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,aAAa;QACb,eAAe;QACf,iBAAiB;QACjB,aAAa;QACb,WAAW;QACX,OAAO;QACP,aAAa;QACb,aAAa;KACd,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/coin-solana",
3
- "version": "0.56.0",
3
+ "version": "0.56.1",
4
4
  "description": "Ledger Solana Coin integration",
5
5
  "keywords": [
6
6
  "Ledger",
@@ -1,5 +1,6 @@
1
1
  import { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer";
2
2
  import { Account, AnyMessage, TypedEvmMessage } from "@ledgerhq/types-live";
3
+ import { LockedDeviceError, UserRefusedOnDevice } from "@ledgerhq/errors";
3
4
  import bs58 from "bs58";
4
5
  import coinConfig from "../../config";
5
6
  import { signMessage } from "../../hw-signMessage";
@@ -20,7 +21,7 @@ describe("Testing call to hardware off-chain sign message on Solana", () => {
20
21
  const BASE58_SIGNATURE =
21
22
  "23mXB2pc8EQzc2mT3VJR4KkBFZaUVL9Pn7LUn8NMKeHKbS1hMj1QqGsUHHD3JMGhAWtFfcmnPhFSpPttChTNzsB9";
22
23
  const BASE58_ENVELOPE =
23
- "LwsiJTXpooGk31Y4CaV1Qs12wTabaF83J9Tg32kPb7kk9BXc8ncpoDBzV1NPSumKckhx7dVwFH729vdvB71f8KGpp18g29N2XAD3MUiNz9tPJu36GGN4pkL7vXurXUeQqMTXdJMdH2tzaXkT3vQ9hSLDfBPhQ7Vx9Echz5CYu5u4hZapaytx177WNke8oWDTqABnqQZ3YDGt7vYDoJ2LkiGHqcqXfeVmmsAtuALSxqo75zrWi7EXadQ9CZWWX7tFnXHLY6kEksqVj2ERM9RZEyNQ3tt";
24
+ "nHpqPBfv1CJUvmpaDaHp4hZZmoucL7f9qFkdwdQj1hFGbLWwWP5rBaBzh2kSFSNJSj5g8yC3Yo5TBHQMCE4ezkQthvpDMpcRvoLkYdwvvDZsqjt3AQnYGzV5fCCrZGnA1pecUUgczB4weoTSQmocrFX2dhqdZ39B71jwWShjn9pW3NY3DtvvMTug9vEkZW8b2GWPbaqoiXMZXAwGQXu4hVBYc8ScN1qV1BXHzX8FCPA";
24
25
 
25
26
  const getAppConfigurationMock = jest.fn(() => {
26
27
  return Promise.resolve({
@@ -44,7 +45,7 @@ describe("Testing call to hardware off-chain sign message on Solana", () => {
44
45
  const freshAddress = "8DpKDisipx6f76cEmuGvCX9TrA3SjeR76HaTRePxHBDe";
45
46
  const messageHex = "54657374696E67206F6E20536F6C616E61";
46
47
  const offchainMessage =
47
- "ff736f6c616e61206f6666636861696e00000000000000000000000000000000000000000000000000000000000000000000016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be03235220035343635373337343639364536373230364636453230353336463643363136453631";
48
+ "ff736f6c616e61206f6666636861696e01016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be0323535343635373337343639364536373230364636453230353336463643363136453631";
48
49
 
49
50
  const result = await signMessage(signerContext)(
50
51
  "",
@@ -104,6 +105,97 @@ describe("Testing call to hardware off-chain sign message on Solana", () => {
104
105
  expect(signMessageMock).toHaveBeenCalledWith(accountFreshAddressPath, offchainMessage);
105
106
  });
106
107
 
108
+ describe("non-legacy fallback sequencing", () => {
109
+ const APP_VERSION = "1.8.2";
110
+ const SIGNATURE =
111
+ "4gVuB1KsM58fb3vRpnDucwW4Vi6fVGA51QDQd9ARvx4GH5yYVDPzDnvzUbSJf3YLWWdsX7zCMSN9N1GMnTYwWiJf";
112
+ const accountFreshAddressPath = "44'/501'/0'/0/0";
113
+ const freshAddress = "8DpKDisipx6f76cEmuGvCX9TrA3SjeR76HaTRePxHBDe";
114
+ const messageHex = "54657374696E67206F6E20536F6C616E61";
115
+
116
+ const V1_NO_LENGTH =
117
+ "ff736f6c616e61206f6666636861696e01016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be0323535343635373337343639364536373230364636453230353336463643363136453631";
118
+ const V1_WITH_LENGTH =
119
+ "ff736f6c616e61206f6666636861696e01016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be03235220035343635373337343639364536373230364636453230353336463643363136453631";
120
+ const V0 =
121
+ "ff736f6c616e61206f6666636861696e00000000000000000000000000000000000000000000000000000000000000000000016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be03235220035343635373337343639364536373230364636453230353336463643363136453631";
122
+
123
+ function makeContext(signMessageMock: jest.Mock): SignerContext<SolanaSigner> {
124
+ const signer: SolanaSigner = {
125
+ getAppConfiguration: jest.fn(() =>
126
+ Promise.resolve({
127
+ version: APP_VERSION,
128
+ blindSigningEnabled: false,
129
+ pubKeyDisplayMode: PubKeyDisplayMode.LONG,
130
+ }),
131
+ ),
132
+ getAddress: jest.fn(),
133
+ signTransaction: jest.fn(),
134
+ signMessage: signMessageMock,
135
+ };
136
+ return (_, fn) => fn(signer);
137
+ }
138
+
139
+ it("falls back from V1 no-length to V1 with-length on format rejection", async () => {
140
+ const signMessageMock = jest
141
+ .fn()
142
+ .mockRejectedValueOnce(new Error("format not supported"))
143
+ .mockResolvedValueOnce({ signature: Buffer.from(SIGNATURE) });
144
+
145
+ await signMessage(makeContext(signMessageMock))(
146
+ "",
147
+ { freshAddressPath: accountFreshAddressPath, freshAddress } as Account,
148
+ { message: messageHex },
149
+ );
150
+
151
+ expect(signMessageMock).toHaveBeenCalledTimes(2);
152
+ expect(signMessageMock).toHaveBeenNthCalledWith(1, accountFreshAddressPath, V1_NO_LENGTH);
153
+ expect(signMessageMock).toHaveBeenNthCalledWith(2, accountFreshAddressPath, V1_WITH_LENGTH);
154
+ });
155
+
156
+ it("falls back all the way to V0 when both V1 variants are rejected", async () => {
157
+ const signMessageMock = jest
158
+ .fn()
159
+ .mockRejectedValueOnce(new Error("format not supported"))
160
+ .mockRejectedValueOnce(new Error("format not supported"))
161
+ .mockResolvedValueOnce({ signature: Buffer.from(SIGNATURE) });
162
+
163
+ await signMessage(makeContext(signMessageMock))(
164
+ "",
165
+ { freshAddressPath: accountFreshAddressPath, freshAddress } as Account,
166
+ { message: messageHex },
167
+ );
168
+
169
+ expect(signMessageMock).toHaveBeenCalledTimes(3);
170
+ expect(signMessageMock).toHaveBeenNthCalledWith(1, accountFreshAddressPath, V1_NO_LENGTH);
171
+ expect(signMessageMock).toHaveBeenNthCalledWith(2, accountFreshAddressPath, V1_WITH_LENGTH);
172
+ expect(signMessageMock).toHaveBeenNthCalledWith(3, accountFreshAddressPath, V0);
173
+ });
174
+
175
+ it.each([
176
+ ["UserRefusedOnDevice", new UserRefusedOnDevice()],
177
+ ["LockedDeviceError", new LockedDeviceError()],
178
+ ["TransportStatusError 0x6985 (user refused)", { statusCode: 0x6985 }],
179
+ ["TransportStatusError 0x5515 (device locked)", { statusCode: 0x5515 }],
180
+ ])("does not retry on %s", async (_label, error) => {
181
+ const signMessageMock = jest.fn().mockRejectedValue(error);
182
+
183
+ let caught: unknown;
184
+ try {
185
+ await signMessage(makeContext(signMessageMock))(
186
+ "",
187
+ { freshAddressPath: accountFreshAddressPath, freshAddress } as Account,
188
+ { message: messageHex },
189
+ );
190
+ } catch (e) {
191
+ caught = e;
192
+ }
193
+
194
+ expect(caught).toBe(error);
195
+ expect(signMessageMock).toHaveBeenCalledTimes(1);
196
+ });
197
+ });
198
+
107
199
  it.each([{} as AnyMessage, {} as TypedEvmMessage])(
108
200
  "should throw an error when message options have not DefaultMessage type",
109
201
  async (messageOptions: AnyMessage) => {
@@ -1,12 +1,29 @@
1
1
  import { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer";
2
2
  import { Account, AnyMessage, DeviceId } from "@ledgerhq/types-live";
3
+ import { LockedDeviceError, UserRefusedOnDevice } from "@ledgerhq/errors";
3
4
  import bs58 from "bs58";
4
5
  import invariant from "invariant";
5
6
  import semver from "semver";
6
7
  import coinConfig from "./config";
7
- import { toOffChainMessage } from "./offchainMessage/format";
8
+ import { toOffChainMessage, toOffChainMessageV1 } from "./offchainMessage/format";
8
9
  import { SolanaSigner } from "./signer";
9
10
 
11
+ // Returns true for device rejections that should trigger a format fallback
12
+ // (e.g. firmware does not support this OCM layout). Propagates user refusals
13
+ // and locked-device errors so they are never silently retried.
14
+ // Handles both DmkSignerSol (maps to typed errors) and LegacySignerSolana
15
+ // (propagates raw TransportStatusError with a numeric statusCode).
16
+ function isFormatRejection(err: unknown): boolean {
17
+ if (err instanceof UserRefusedOnDevice) return false;
18
+ if (err instanceof LockedDeviceError) return false;
19
+ // TransportStatusError duck-type: 0x6985 = user refused, 0x5515 = device locked
20
+ if (err && typeof err === "object" && "statusCode" in err) {
21
+ const { statusCode } = err as { statusCode: number };
22
+ if (statusCode === 0x6985 || statusCode === 0x5515) return false;
23
+ }
24
+ return true;
25
+ }
26
+
10
27
  export const signMessage =
11
28
  (signerContext: SignerContext<SolanaSigner>) =>
12
29
  async (
@@ -29,9 +46,42 @@ export const signMessage =
29
46
  const { version } = await signer.getAppConfiguration();
30
47
  const isLegacy = semver.lt(version, coinConfig.getCoinConfig().legacyOCMSMaxVersion);
31
48
 
32
- signedMessage = toOffChainMessage(message, account.freshAddress, isLegacy);
49
+ if (isLegacy) {
50
+ const ocm = toOffChainMessage(message, account.freshAddress, true);
51
+ signedMessage = ocm;
52
+ return signer.signMessage(account.freshAddressPath, ocm.toString("hex"));
53
+ }
54
+
55
+ // Step 1 — V1 without length prefix (finalised sRFC 38 spec).
56
+ const v1NoLength = toOffChainMessageV1(message, account.freshAddress, false);
57
+ try {
58
+ const sig = await signer.signMessage(
59
+ account.freshAddressPath,
60
+ v1NoLength.toString("hex"),
61
+ );
62
+ signedMessage = v1NoLength;
63
+ return sig;
64
+ } catch (err) {
65
+ if (!isFormatRejection(err)) throw err;
66
+ }
67
+
68
+ // Step 2 — V1 with length prefix (pre-sRFC-38 firmware).
69
+ const v1WithLength = toOffChainMessageV1(message, account.freshAddress, true);
70
+ try {
71
+ const sig = await signer.signMessage(
72
+ account.freshAddressPath,
73
+ v1WithLength.toString("hex"),
74
+ );
75
+ signedMessage = v1WithLength;
76
+ return sig;
77
+ } catch (err) {
78
+ if (!isFormatRejection(err)) throw err;
79
+ }
33
80
 
34
- return signer.signMessage(account.freshAddressPath, signedMessage.toString("hex"));
81
+ // Step 3 — V0 fallback (older firmware that does not support V1 at all).
82
+ const v0 = toOffChainMessage(message, account.freshAddress, false);
83
+ signedMessage = v0;
84
+ return signer.signMessage(account.freshAddressPath, v0.toString("hex"));
35
85
  });
36
86
 
37
87
  invariant(signedMessage, "signedMessage should exist");
@@ -6,11 +6,9 @@
6
6
 
7
7
  import { PublicKey } from "@solana/web3.js";
8
8
 
9
- // Max off-chain message length supported by Ledger
9
+ // Max off-chain message length supported by Ledger (= app-solana MAX_OFFCHAIN_MESSAGE_LENGTH)
10
10
  const OFFCM_MAX_LEDGER_LEN = 15 * 1024 - 40 - 8;
11
11
  const LEGACY_OFFCM_MAX_LEDGER_LEN = 1280 - 40 - 8;
12
- // Max length of version 0 off-chain message
13
- const OFFCM_MAX_V0_LEN = 65515;
14
12
 
15
13
  const MAX_PRINTABLE_ASCII = 0x7e;
16
14
  const MIN_PRINTABLE_ASCII = 0x20;
@@ -37,32 +35,72 @@ function isUTF8(buffer: Buffer) {
37
35
 
38
36
  function findMessageFormat(messageBuffer: Buffer, isLegacy: boolean): number {
39
37
  const maxLedgerLen = isLegacy ? LEGACY_OFFCM_MAX_LEDGER_LEN : OFFCM_MAX_LEDGER_LEN;
40
-
41
- if (messageBuffer.length <= maxLedgerLen) {
42
- if (isPrintableASCII(messageBuffer, isLegacy)) {
43
- return 0;
44
- } else if (isUTF8(messageBuffer)) {
45
- return 1;
46
- }
47
- } else if (messageBuffer.length <= OFFCM_MAX_V0_LEN) {
48
- if (isUTF8(messageBuffer)) {
49
- return 2;
50
- }
38
+ if (messageBuffer.length > maxLedgerLen) {
39
+ throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${maxLedgerLen})`);
51
40
  }
41
+ if (isPrintableASCII(messageBuffer, isLegacy)) return 0;
42
+ if (isUTF8(messageBuffer)) return 1;
52
43
  return 0;
53
44
  }
54
45
 
46
+ const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
47
+
55
48
  /**
56
- * 1. Signing domain (16 bytes)
57
- * 2. Header version (1 byte)
58
- * 3. Application domain (32 bytes)
59
- * 4. Message format (1 byte)
60
- * 5. Signer count (1 bytes)
61
- * 6. Signers (signer_count * 32 bytes) - assume that only one signer is present
62
- * 7. Message length (2 bytes)
49
+ * V1 wire format (sRFC 38, per app-solana parser):
50
+ * signing domain (16 B): 0xFF + "solana offchain"
51
+ * version ( 1 B): 0x01
52
+ * signer count ( 1 B): 1
53
+ * signer (32 B)
54
+ * [message length ( 2 B): little-endian uint16] only when includeLengthPrefix
55
+ * message body (variable)
56
+ *
57
+ * The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
58
+ * reproduces the pre-spec-update layout for backward compatibility with older
59
+ * firmware that rejects the no-prefix form with 6a81.
63
60
  */
64
- const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
65
- const headerVersion = Buffer.alloc(1);
61
+ export function toOffChainMessageV1(
62
+ message: string,
63
+ signerAddress: string,
64
+ includeLengthPrefix = false,
65
+ ): Buffer {
66
+ const messageBuffer = Buffer.from(message);
67
+ if (messageBuffer.length > OFFCM_MAX_LEDGER_LEN) {
68
+ throw new Error(
69
+ `Message too long: ${messageBuffer.length} bytes (max is ${OFFCM_MAX_LEDGER_LEN})`,
70
+ );
71
+ }
72
+
73
+ const version = Buffer.from([0x01]);
74
+ const signerCount = Buffer.from([0x01]);
75
+ const signer = new PublicKey(signerAddress).toBuffer();
76
+
77
+ if (includeLengthPrefix) {
78
+ const messageLength = Buffer.alloc(2);
79
+ messageLength.writeUInt16LE(messageBuffer.length);
80
+ return Buffer.concat([signingDomain, version, signerCount, signer, messageLength, messageBuffer]);
81
+ }
82
+ return Buffer.concat([signingDomain, version, signerCount, signer, messageBuffer]);
83
+ }
84
+
85
+ /**
86
+ * V0 wire format:
87
+ * signing domain (16 B): 0xFF + "solana offchain"
88
+ * version ( 1 B): 0x00
89
+ * application domain (32 B): zero-padded
90
+ * format ( 1 B): 0 = ASCII, 1 = UTF-8
91
+ * signer count ( 1 B): 1
92
+ * signer (32 B)
93
+ * message length ( 2 B): little-endian uint16
94
+ * message body (variable)
95
+ *
96
+ * Legacy format (Nano S / old firmware):
97
+ * signing domain (16 B)
98
+ * version ( 1 B): 0x00
99
+ * format ( 1 B)
100
+ * message length ( 2 B): always present
101
+ * message body (variable)
102
+ */
103
+ const v0HeaderVersion = Buffer.alloc(1); // 0x00
66
104
  const applicationDomain = Buffer.alloc(32);
67
105
  const messageFormat = Buffer.alloc(1);
68
106
  const signerCount = Buffer.alloc(1);
@@ -79,20 +117,26 @@ export function toOffChainMessage(
79
117
 
80
118
  const signers = new PublicKey(signerAddress).toBuffer();
81
119
 
82
- messageLength.writeUint16LE(messageBuffer.length);
120
+ messageLength.writeUInt16LE(messageBuffer.length);
121
+
122
+ if (isLegacy) {
123
+ return Buffer.concat([
124
+ signingDomain,
125
+ v0HeaderVersion,
126
+ messageFormat,
127
+ messageLength,
128
+ messageBuffer,
129
+ ]);
130
+ }
83
131
 
84
- return Buffer.concat(
85
- isLegacy
86
- ? [signingDomain, headerVersion, messageFormat, messageLength, messageBuffer]
87
- : [
88
- signingDomain,
89
- headerVersion,
90
- applicationDomain,
91
- messageFormat,
92
- signerCount,
93
- signers,
94
- messageLength,
95
- messageBuffer,
96
- ],
97
- );
132
+ return Buffer.concat([
133
+ signingDomain,
134
+ v0HeaderVersion,
135
+ applicationDomain,
136
+ messageFormat,
137
+ signerCount,
138
+ signers,
139
+ messageLength,
140
+ messageBuffer,
141
+ ]);
98
142
  }
@@ -1,5 +1,5 @@
1
1
  import { PublicKey } from "@solana/web3.js";
2
- import { toOffChainMessage } from "./format";
2
+ import { toOffChainMessage, toOffChainMessageV1 } from "./format";
3
3
 
4
4
  type MessageCase = {
5
5
  value: string;
@@ -78,8 +78,9 @@ Issued At: now`,
78
78
  const addressHex = new PublicKey(address).toBuffer().toString("hex");
79
79
  const emptyApplicationDomain = "0000000000000000000000000000000000000000000000000000000000000000";
80
80
 
81
+ // V0 layout: header(32) + version(2) + appDomain(64) + format(2) + signerCount(2) + signer(64) + length(4) + body
81
82
  it.each(cases)(
82
- 'should format message "$value" correctly for off-chain signing on Solana',
83
+ 'should format message "$value" correctly for off-chain signing on Solana (V0)',
83
84
  ({ value, hexadecimal, messageFormat, messageLength }) => {
84
85
  const result = toOffChainMessage(value, address, false).toString("hex");
85
86
 
@@ -106,11 +107,56 @@ Issued At: now`,
106
107
  const length = result.substring(SOLANA_HEADER.length + 134, SOLANA_HEADER.length + 138);
107
108
  expect(length).toEqual(messageLength);
108
109
 
109
- // message should end with message parameter
110
110
  expect(result.substring(SOLANA_HEADER.length + 138)).toEqual(hexadecimal);
111
111
  },
112
112
  );
113
113
 
114
+ // V1 layout (no length): header(32) + version(2) + signerCount(2) + signer(64) + body
115
+ it.each(cases)(
116
+ 'should format message "$value" correctly for off-chain signing on Solana (V1, no length)',
117
+ ({ value, hexadecimal, messageLength: _ignored }) => {
118
+ const result = toOffChainMessageV1(value, address).toString("hex");
119
+
120
+ expect(result.startsWith(SOLANA_HEADER)).toBe(true);
121
+
122
+ const version = result.substring(SOLANA_HEADER.length, SOLANA_HEADER.length + 2);
123
+ expect(version).toEqual("01");
124
+
125
+ const signerCount = result.substring(SOLANA_HEADER.length + 2, SOLANA_HEADER.length + 4);
126
+ expect(signerCount).toEqual("01");
127
+
128
+ const signerAddress = result.substring(SOLANA_HEADER.length + 4, SOLANA_HEADER.length + 68);
129
+ expect(signerAddress).toEqual(addressHex);
130
+
131
+ // no length field — message body starts immediately after signer
132
+ expect(result.substring(SOLANA_HEADER.length + 68)).toEqual(hexadecimal);
133
+ },
134
+ );
135
+
136
+ // V1 layout (with length): header(32) + version(2) + signerCount(2) + signer(64) + length(4) + body
137
+ it.each(cases)(
138
+ 'should format message "$value" correctly for off-chain signing on Solana (V1, with length prefix)',
139
+ ({ value, hexadecimal, messageLength }) => {
140
+ const result = toOffChainMessageV1(value, address, true).toString("hex");
141
+
142
+ expect(result.startsWith(SOLANA_HEADER)).toBe(true);
143
+
144
+ const version = result.substring(SOLANA_HEADER.length, SOLANA_HEADER.length + 2);
145
+ expect(version).toEqual("01");
146
+
147
+ const signerCount = result.substring(SOLANA_HEADER.length + 2, SOLANA_HEADER.length + 4);
148
+ expect(signerCount).toEqual("01");
149
+
150
+ const signerAddress = result.substring(SOLANA_HEADER.length + 4, SOLANA_HEADER.length + 68);
151
+ expect(signerAddress).toEqual(addressHex);
152
+
153
+ const length = result.substring(SOLANA_HEADER.length + 68, SOLANA_HEADER.length + 72);
154
+ expect(length).toEqual(messageLength);
155
+
156
+ expect(result.substring(SOLANA_HEADER.length + 72)).toEqual(hexadecimal);
157
+ },
158
+ );
159
+
114
160
  const legacyCases: MessageCase[] = [
115
161
  ...sharedCases,
116
162
  {