-
Notifications
You must be signed in to change notification settings - Fork 24
β¨ (solana-signer) [DSDK-1098]: Solana transaction check & Solana provide context refactor #1481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b9a9866
c660008
4b36b4e
a9913b1
5da4263
2b8241d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@ledgerhq/device-signer-kit-solana": minor | ||
| --- | ||
|
|
||
| Add transaction check | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@ledgerhq/device-management-kit": minor | ||
| --- | ||
|
|
||
| Extend `ApplicationChecker` with app-name aware constraints: track the current running app from the device session state and expose `excludeApp`, `excludeApps`, and `excludeDeviceModels` builder methods. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,13 +12,16 @@ | |
| private isCompatible: boolean; | ||
| private version: string; | ||
| private modelId: DeviceModelId; | ||
| private readonly appName: string | undefined; | ||
|
|
||
| constructor( | ||
| deviceState: DeviceSessionState, | ||
| appConfig: AppConfig, | ||
| resolver: ApplicationResolver, | ||
| ) { | ||
| this.modelId = deviceState.deviceModelId; | ||
| this.appName = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ASK] why appName can be undefined?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the state is connected but not ready yet we should not enter in this part of code no? because we have the same problem with other signers, wdyt?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree it's an issue across signers! Currently harmless here because |
||
| "currentApp" in deviceState ? deviceState.currentApp.name : undefined; | ||
| const resolved = resolver.resolve(deviceState, appConfig); | ||
| this.isCompatible = resolved.isCompatible; | ||
| this.version = resolved.version; | ||
|
|
@@ -39,6 +42,21 @@ | |
| return this; | ||
| } | ||
|
|
||
| excludeDeviceModels(...modelIds: DeviceModelId[]): ApplicationChecker { | ||
|
Check warning on line 45 in packages/device-management-kit/src/api/utils/ApplicationChecker.ts
|
||
| for (const id of modelIds) this.excludeDeviceModel(id); | ||
| return this; | ||
| } | ||
|
|
||
| excludeApp(name: string): ApplicationChecker { | ||
|
Check warning on line 50 in packages/device-management-kit/src/api/utils/ApplicationChecker.ts
|
||
| if (this.appName === name) this.isCompatible = false; | ||
| return this; | ||
| } | ||
|
|
||
| excludeApps(...names: string[]): ApplicationChecker { | ||
|
Check warning on line 55 in packages/device-management-kit/src/api/utils/ApplicationChecker.ts
|
||
| for (const name of names) this.excludeApp(name); | ||
| return this; | ||
| } | ||
|
|
||
| check(): boolean { | ||
| return this.isCompatible; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { ContainerModule } from "inversify"; | ||
|
|
||
| import { HttpTransactionCheckDataSource } from "@/modules/multichain/transaction-check/data/HttpTransactionCheckDataSource"; | ||
| import { transactionCheckTypes } from "@/modules/multichain/transaction-check/di/transactionCheckTypes"; | ||
| import { SolanaTransactionCheckLoader } from "@/modules/multichain/transaction-check/loaders/SolanaTransactionCheckLoader"; | ||
|
|
||
| export const solanaTransactionCheckModuleFactory = () => | ||
| new ContainerModule(({ bind }) => { | ||
| bind(transactionCheckTypes.TransactionCheckDataSource).to( | ||
| HttpTransactionCheckDataSource, | ||
| ); | ||
| bind(transactionCheckTypes.TransactionCheckLoader).to( | ||
| SolanaTransactionCheckLoader, | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { | ||
| DeviceModelId, | ||
| LoggerPublisherService, | ||
| } from "@ledgerhq/device-management-kit"; | ||
| import { inject, injectable } from "inversify"; | ||
| import { Codec, exactly, number, oneOf, string } from "purify-ts"; | ||
|
|
||
| import { configTypes } from "@/config/di/configTypes"; | ||
| import { pkiTypes } from "@/modules/multichain/pki/di/pkiTypes"; | ||
| import { type PkiCertificateLoader } from "@/modules/multichain/pki/domain/PkiCertificateLoader"; | ||
| import { KeyUsage } from "@/modules/multichain/pki/model/KeyUsage"; | ||
| import { type TransactionCheckDataSource } from "@/modules/multichain/transaction-check/data/TransactionCheckDataSource"; | ||
| import { transactionCheckTypes } from "@/modules/multichain/transaction-check/di/transactionCheckTypes"; | ||
| import { type TransactionCheckLoader } from "@/modules/multichain/transaction-check/loaders/TransactionCheckLoader"; | ||
| import { TransactionCheckPaths } from "@/modules/multichain/transaction-check/utils/constants"; | ||
| import { | ||
| ClearSignContext, | ||
| ClearSignContextType, | ||
| } from "@/shared/model/ClearSignContext"; | ||
|
|
||
| export type SolanaTransactionCheckRequest = { | ||
| from: string; | ||
| rawTx: string; | ||
| chain: number; | ||
| }; | ||
|
|
||
| export type SolanaTransactionCheckContextInput = { | ||
| deviceModelId: DeviceModelId; | ||
| transactionCheck: SolanaTransactionCheckRequest; | ||
| }; | ||
|
|
||
| const SUPPORTED_TYPES: ClearSignContextType[] = [ | ||
| ClearSignContextType.SOLANA_TRANSACTION_CHECK, | ||
| ]; | ||
|
|
||
| const solanaTransactionCheckInputCodec = Codec.interface({ | ||
| deviceModelId: oneOf([ | ||
| exactly(DeviceModelId.NANO_X), | ||
| exactly(DeviceModelId.NANO_SP), | ||
| exactly(DeviceModelId.STAX), | ||
| exactly(DeviceModelId.FLEX), | ||
| ]), | ||
| transactionCheck: Codec.interface({ | ||
| from: string, | ||
| rawTx: string, | ||
| chain: number, | ||
| }), | ||
| }); | ||
|
|
||
| @injectable() | ||
| export class SolanaTransactionCheckLoader | ||
| implements TransactionCheckLoader<SolanaTransactionCheckContextInput> | ||
| { | ||
| private readonly logger: LoggerPublisherService; | ||
|
|
||
| constructor( | ||
| @inject(transactionCheckTypes.TransactionCheckDataSource) | ||
| private readonly transactionCheckDataSource: TransactionCheckDataSource, | ||
| @inject(pkiTypes.PkiCertificateLoader) | ||
| private readonly certificateLoader: PkiCertificateLoader, | ||
| @inject(configTypes.ContextModuleLoggerFactory) | ||
| loggerFactory: (tag: string) => LoggerPublisherService, | ||
| ) { | ||
| this.logger = loggerFactory("SolanaTransactionCheckLoader"); | ||
| } | ||
|
|
||
| canHandle( | ||
| input: unknown, | ||
|
fAnselmi-Ledger marked this conversation as resolved.
|
||
| expectedType: ClearSignContextType[], | ||
| ): input is SolanaTransactionCheckContextInput { | ||
| if (!SUPPORTED_TYPES.every((type) => expectedType.includes(type))) | ||
| return false; | ||
| return solanaTransactionCheckInputCodec.decode(input).caseOf({ | ||
| Left: () => false, | ||
| Right: ({ transactionCheck: { from, rawTx } }) => | ||
| from.length > 0 && rawTx.length > 0, | ||
| }); | ||
| } | ||
|
|
||
| async load( | ||
| ctx: SolanaTransactionCheckContextInput, | ||
| ): Promise<ClearSignContext[]> { | ||
| const { from, rawTx, chain } = ctx.transactionCheck; | ||
|
|
||
| const txCheck = await this.transactionCheckDataSource.check({ | ||
| path: TransactionCheckPaths.SOLANA_TRANSACTION, | ||
| body: { tx: { from, raw: rawTx }, chain }, | ||
| }); | ||
|
|
||
| const context = await txCheck.caseOf<Promise<ClearSignContext>>({ | ||
| Left: (error) => | ||
| Promise.resolve({ | ||
| type: ClearSignContextType.ERROR, | ||
| error, | ||
| }), | ||
| Right: async (data) => { | ||
| const certificate = await this.certificateLoader.loadCertificate({ | ||
| keyId: data.publicKeyId, | ||
| keyUsage: KeyUsage.TxSimulationSigner, | ||
| targetDevice: ctx.deviceModelId, | ||
| }); | ||
|
|
||
| return { | ||
| type: ClearSignContextType.SOLANA_TRANSACTION_CHECK, | ||
| payload: { descriptor: data.descriptor }, | ||
| certificate, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| const result = [context]; | ||
| this.logger.debug("load result", { data: { result } }); | ||
| return result; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| export enum TransactionCheckPaths { | ||
| ETHEREUM_TRANSACTION = "/ethereum/scan/tx", | ||
| ETHEREUM_TYPED_DATA = "/ethereum/scan/eip-712", | ||
| SOLANA_TRANSACTION = "/solana/scan/tx", | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /** | ||
| * Web3Checks scan chain identifiers for Solana clusters. | ||
| * Mirrors the backend contract for the `chain` field on Solana scan requests. | ||
| */ | ||
| export enum SolanaTransactionScanChainId { | ||
| MAINNET = 1, | ||
| DEVNET = 2, | ||
| TESTNET = 3, | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.