Skip to content

Commit faab51d

Browse files
✨ (signer-solana): Restructure sign transaction device action with clear sign child machines
1 parent 5cd9220 commit faab51d

20 files changed

Lines changed: 2766 additions & 2554 deletions

.changeset/pink-mangos-sin-two.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ledgerhq/context-module": minor
3+
---
4+
5+
Support changes for generic clear sign device action

.changeset/pink-mangos-sin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ledgerhq/device-signer-kit-solana": minor
3+
---
4+
5+
Add generic clear-sign device action

packages/signer/signer-solana/src/api/app-binder/SignTransactionDeviceActionTypes.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import {
2-
type ContextModule,
3-
type SolanaTransactionContextResultSuccess,
4-
} from "@ledgerhq/context-module";
1+
import { type ContextModule } from "@ledgerhq/context-module";
52
import {
63
type DeviceActionState,
74
type ExecuteDeviceActionReturnType,
@@ -17,26 +14,40 @@ import { type SolanaTransactionOptionalConfig } from "@api/model/SolanaTransacti
1714
import { type Transaction } from "@api/model/Transaction";
1815
import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors";
1916
import { type BlockhashService } from "@internal/app-binder/services/BlockhashService";
20-
import { type TxInspectorResult } from "@internal/app-binder/services/TransactionInspector";
2117

22-
import { type DelayedSignDAStateStep } from "./DelayedSignTransactionDeviceActionTypes";
18+
import { type SigningOperationsDAStateStep } from "./SigningOperationsDeviceActionTypes";
2319

2420
export const signTransactionDAStateSteps = Object.freeze({
2521
OPEN_APP: "signer.sol.steps.openApp",
2622
GET_APP_CONFIG: "signer.sol.steps.getAppConfig",
2723
WEB3_CHECKS_OPT_IN: "signer.sol.steps.web3ChecksOptIn",
2824
WEB3_CHECKS_OPT_IN_RESULT: "signer.sol.steps.web3ChecksOptInResult",
25+
WEB3_CHECKS_PROVIDE: "signer.sol.steps.web3ChecksProvide",
2926
INSPECT_TRANSACTION: "signer.sol.steps.inspectTransaction",
3027
GET_PUB_KEY: "signer.sol.steps.getPubKey",
31-
BUILD_TRANSACTION_CONTEXT: "signer.sol.steps.buildTransactionContext",
32-
PROVIDE_TRANSACTION_CONTEXT: "signer.sol.steps.provideTransactionContext",
28+
BUILD_BASIC_CLEAR_SIGN_CONTEXT: "signer.sol.steps.buildBasicClearSignContext",
29+
PROVIDE_BASIC_CLEAR_SIGN_CONTEXT:
30+
"signer.sol.steps.provideBasicClearSignContext",
31+
BUILD_GENERIC_CLEAR_SIGN_CONTEXT:
32+
"signer.sol.steps.buildGenericClearSignContext",
33+
PROVIDE_GENERIC_CLEAR_SIGN_CONTEXT:
34+
"signer.sol.steps.provideGenericClearSignContext",
35+
PROMPT_UI_DISPLAY: "signer.sol.steps.promptUiDisplay",
3336
SIGN_TRANSACTION: "signer.sol.steps.signTransaction",
3437
DELAYED_SIGN: "signer.sol.steps.delayedSign",
3538
} as const);
3639

40+
/**
41+
* Which clear-signing path produced the signature.
42+
* `full` = device ran the merge engine
43+
* `srfc39-only` = partial CAL coverage, device auto-rendered per-instruction without merge
44+
* `none` = blind/legacy fallback (no instruction recognised or capability absent).
45+
*/
46+
export type ClearSignMode = "full" | "srfc39-only" | "none";
47+
3748
export type SignTransactionDAStateStep =
3849
| (typeof signTransactionDAStateSteps)[keyof typeof signTransactionDAStateSteps]
39-
| DelayedSignDAStateStep;
50+
| SigningOperationsDAStateStep;
4051

4152
export type SignTransactionDAOutput = Signature;
4253

@@ -81,9 +92,9 @@ export type SignTransactionDAInternalState = {
8192
readonly error: SignTransactionDAError | null;
8293
readonly signature: Signature | null;
8394
readonly appConfig: AppConfiguration | null;
84-
readonly solanaTransactionContext: SolanaTransactionContextResultSuccess | null;
85-
readonly inspectorResult: TxInspectorResult | null;
86-
readonly signerAddress: string | null;
95+
// Set when the generic clear-sign path armed the device, so the terminal
96+
// sign skips its own preview.
97+
readonly clearSignArmed: boolean;
8798
};
8899

89100
export type SignTransactionDAReturnType = ExecuteDeviceActionReturnType<

packages/signer/signer-solana/src/api/app-binder/DelayedSignTransactionDeviceActionTypes.ts renamed to packages/signer/signer-solana/src/api/app-binder/SigningOperationsDeviceActionTypes.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,59 +12,65 @@ import { type UserInputType } from "@api/model/TransactionResolutionContext";
1212
import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors";
1313
import { type BlockhashService } from "@internal/app-binder/services/BlockhashService";
1414

15-
export const delayedSignDAStateSteps = Object.freeze({
15+
export const signingOperationsDAStateSteps = Object.freeze({
1616
ZERO_BLOCKHASH: "signer.sol.steps.zeroBlockhash",
1717
PREVIEW_TRANSACTION: "signer.sol.steps.previewTransaction",
1818
FETCH_BLOCKHASH: "signer.sol.steps.fetchBlockhash",
1919
PATCH_TRANSACTION: "signer.sol.steps.patchTransaction",
2020
DELAYED_SIGN: "signer.sol.steps.delayedSign",
21+
SIGN_TRANSACTION: "signer.sol.steps.signTransaction",
2122
FALLBACK_TO_NON_DELAYED_SIGN: "signer.sol.steps.fallbackToNonDelayedSign",
2223
} as const);
2324

24-
export type DelayedSignDAStateStep =
25-
(typeof delayedSignDAStateSteps)[keyof typeof delayedSignDAStateSteps];
25+
export type SigningOperationsDAStateStep =
26+
(typeof signingOperationsDAStateSteps)[keyof typeof signingOperationsDAStateSteps];
2627

27-
export type DelayedSignDAOutput = Signature;
28+
export type SigningOperationsDAOutput = Signature;
2829

29-
export type DelayedSignDAInput = {
30+
export type SigningOperationsDAInput = {
3031
readonly derivationPath: string;
3132
readonly transaction: Uint8Array;
3233
readonly rpcUrl?: string;
3334
readonly fetchBlockhash?: () => Promise<Uint8Array>;
3435
readonly userInputType?: UserInputType;
3536
readonly blockhashService?: BlockhashService;
37+
// When the device fingerprint is already armed (e.g. by generic clear-sign's
38+
// PROMPT UI DISPLAY), skip the SIGN MESSAGE PREVIEW step and go straight to
39+
// the blockhash refresh + SIGN MESSAGE DELAYED. Without a blockhash source the
40+
// original transaction is signed as-is.
41+
readonly alreadyArmed?: boolean;
3642
};
3743

38-
export type DelayedSignDAError =
44+
export type SigningOperationsDAError =
3945
| OpenAppDAError
4046
| SendCommandInAppDAError<SolanaAppErrorCodes>;
4147

42-
type DelayedSignDARequiredInteraction =
48+
type SigningOperationsDARequiredInteraction =
4349
| UserInteractionRequired
4450
| OpenAppDARequiredInteraction;
4551

46-
export type DelayedSignDAIntermediateValue = {
47-
requiredUserInteraction: DelayedSignDARequiredInteraction;
48-
step: DelayedSignDAStateStep;
52+
export type SigningOperationsDAIntermediateValue = {
53+
requiredUserInteraction: SigningOperationsDARequiredInteraction;
54+
step: SigningOperationsDAStateStep;
4955
};
5056

51-
export type DelayedSignDAState = DeviceActionState<
52-
DelayedSignDAOutput,
53-
DelayedSignDAError,
54-
DelayedSignDAIntermediateValue
57+
export type SigningOperationsDAState = DeviceActionState<
58+
SigningOperationsDAOutput,
59+
SigningOperationsDAError,
60+
SigningOperationsDAIntermediateValue
5561
>;
5662

57-
export type DelayedSignDAInternalState = {
58-
readonly error: DelayedSignDAError | null;
63+
export type SigningOperationsDAInternalState = {
64+
readonly error: SigningOperationsDAError | null;
5965
readonly signature: Signature | null;
6066
readonly zeroedTransaction: Uint8Array | null;
6167
readonly freshBlockhash: Uint8Array | null;
6268
readonly patchedTransaction: Uint8Array | null;
6369
readonly previewFallback: boolean;
6470
};
6571

66-
export type DelayedSignDAReturnType = ExecuteDeviceActionReturnType<
67-
DelayedSignDAOutput,
68-
DelayedSignDAError,
69-
DelayedSignDAIntermediateValue
72+
export type SigningOperationsDAReturnType = ExecuteDeviceActionReturnType<
73+
SigningOperationsDAOutput,
74+
SigningOperationsDAError,
75+
SigningOperationsDAIntermediateValue
7076
>;

packages/signer/signer-solana/src/internal/app-binder/SolanaApplicationResolver.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import {
22
type AppConfig,
3+
ApplicationChecker,
34
type ApplicationResolver,
45
DeviceModelId,
56
type DeviceSessionState,
67
DeviceSessionStateType,
8+
type InternalApi,
79
type ResolvedApp,
810
} from "@ledgerhq/device-management-kit";
911

12+
import { type AppConfiguration } from "@api/model/AppConfiguration";
13+
1014
import { APP_NAME } from "./constants";
1115

16+
const UNRELEASED_MIN_VERSION = "10.0.0";
1217
const DEFAULT_VERSION = "0.0.1";
1318
export const SOLANA_MIN_SPL_VERSION = "1.9.2";
1419
export const SOLANA_MIN_DELAYED_SIGNING_VERSION = "1.14.0";
15-
export const SOLANA_MIN_WEB3_CHECKS_VERSION = "1.15.0";
20+
21+
export const SOLANA_MIN_WEB3_CHECKS_VERSION = UNRELEASED_MIN_VERSION;
22+
export const SOLANA_MIN_GENERIC_CLEAR_SIGN_VERSION = UNRELEASED_MIN_VERSION;
1623

1724
export const SOLANA_FEATURES = {
1825
spl: {
@@ -34,8 +41,34 @@ export const SOLANA_FEATURES = {
3441
excludedModels: [] as DeviceModelId[],
3542
excludedApps: [] as string[],
3643
},
44+
genericClearSign: {
45+
minVersion: SOLANA_MIN_GENERIC_CLEAR_SIGN_VERSION,
46+
excludedModels: [DeviceModelId.NANO_S],
47+
excludedApps: ["Exchange"],
48+
},
3749
} as const;
3850

51+
/**
52+
* Whether the connected Solana app supports a given feature, applying its
53+
* minimum version plus device-model / orchestrating-app exclusions.
54+
*/
55+
export function isSolanaFeatureSupported(
56+
internalApi: InternalApi,
57+
feature: keyof typeof SOLANA_FEATURES,
58+
appConfig: AppConfiguration,
59+
): boolean {
60+
const { minVersion, excludedModels, excludedApps } = SOLANA_FEATURES[feature];
61+
return new ApplicationChecker(
62+
internalApi.getDeviceSessionState(),
63+
appConfig,
64+
new SolanaApplicationResolver(),
65+
)
66+
.withMinVersionInclusive(minVersion)
67+
.excludeDeviceModels(...excludedModels)
68+
.excludeApps(...excludedApps)
69+
.check();
70+
}
71+
3972
export class SolanaApplicationResolver implements ApplicationResolver {
4073
resolve(deviceState: DeviceSessionState, appConfig: AppConfig): ResolvedApp {
4174
if (deviceState.sessionStateType === DeviceSessionStateType.Connected) {

0 commit comments

Comments
 (0)