Skip to content

Commit 86d611e

Browse files
✨ (solana-signer) [DSDK-1098]: Solana transaction check & Solana provide context refactor (#1481)
2 parents ebcbbeb + 2b8241d commit 86d611e

42 files changed

Lines changed: 2771 additions & 1859 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/large-stars-relate.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 transaction check

.changeset/swift-otters-roam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ledgerhq/device-management-kit": minor
3+
---
4+
5+
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.

packages/device-management-kit/src/api/utils/ApplicationChecker.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ export class ApplicationChecker {
1212
private isCompatible: boolean;
1313
private version: string;
1414
private modelId: DeviceModelId;
15+
private readonly appName: string | undefined;
1516

1617
constructor(
1718
deviceState: DeviceSessionState,
1819
appConfig: AppConfig,
1920
resolver: ApplicationResolver,
2021
) {
2122
this.modelId = deviceState.deviceModelId;
23+
this.appName =
24+
"currentApp" in deviceState ? deviceState.currentApp.name : undefined;
2225
const resolved = resolver.resolve(deviceState, appConfig);
2326
this.isCompatible = resolved.isCompatible;
2427
this.version = resolved.version;
@@ -39,6 +42,21 @@ export class ApplicationChecker {
3942
return this;
4043
}
4144

45+
excludeDeviceModels(...modelIds: DeviceModelId[]): ApplicationChecker {
46+
for (const id of modelIds) this.excludeDeviceModel(id);
47+
return this;
48+
}
49+
50+
excludeApp(name: string): ApplicationChecker {
51+
if (this.appName === name) this.isCompatible = false;
52+
return this;
53+
}
54+
55+
excludeApps(...names: string[]): ApplicationChecker {
56+
for (const name of names) this.excludeApp(name);
57+
return this;
58+
}
59+
4260
check(): boolean {
4361
return this.isCompatible;
4462
}

packages/signer/context-module/src/DefaultContextModule.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ export class DefaultContextModule implements ContextModule {
121121
this._container.get<ContextLoader>(
122122
ownerInfoTypes.OwnerInfoContextLoader,
123123
),
124+
this._container.get<ContextLoader>(
125+
transactionCheckTypes.TransactionCheckLoader,
126+
),
124127
];
125128
case ContextModuleChainID.Concordium:
126129
return [

packages/signer/context-module/src/di.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { uniswapModuleFactory } from "@/modules/ethereum/uniswap/di/uniswapModul
2222
import { nanoPkiModuleFactory } from "@/modules/multichain/pki/di/pkiModuleFactory";
2323
import { reporterModuleFactory } from "@/modules/multichain/reporter/di/reporterModuleFactory";
2424
import { ethereumTransactionCheckModuleFactory } from "@/modules/multichain/transaction-check/di/ethereumTransactionCheckModuleFactory";
25+
import { solanaTransactionCheckModuleFactory } from "@/modules/multichain/transaction-check/di/solanaTransactionCheckModuleFactory";
2526
import { lifiModuleFactory } from "@/modules/solana/lifi/di/lifiModuleFactory";
2627
import { ownerInfoModuleFactory } from "@/modules/solana/owner-info/di/ownerInfoModuleFactory";
2728
import { tokenModuleFactory as solanaTokenModuleFactory } from "@/modules/solana/token/di/tokenModuleFactory";
@@ -71,6 +72,7 @@ export const makeContainer = ({ config }: MakeContainerArgs) => {
7172
ownerInfoModuleFactory(),
7273
solanaTokenModuleFactory(),
7374
lifiModuleFactory(),
75+
solanaTransactionCheckModuleFactory(),
7476
);
7577
break;
7678
case ContextModuleChainID.Concordium:

packages/signer/context-module/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ export * from "./modules/multichain/reporter/domain/BlindSigningReporter";
7272
export * from "./modules/multichain/reporter/domain/DefaultBlindSigningReporter";
7373
export * from "./modules/multichain/reporter/model/BlindSigningEvent";
7474
export * from "./modules/multichain/reporter/model/BlindSigningModelId";
75+
export * from "./modules/multichain/transaction-check/utils/constants";
7576
export * from "./modules/solana/model/SolanaClearSignContext";
7677
export * from "./modules/solana/model/SolanaContextTypes";
78+
export * from "./modules/solana/model/SolanaTransactionScanChainId";
7779
export * from "./modules/solana/owner-info/data/HttpOwnerInfoDataSource";
7880
export * from "./modules/solana/owner-info/data/OwnerInfoDataSource";
7981
export * from "./modules/solana/owner-info/domain/OwnerInfoContextLoader";
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ContainerModule } from "inversify";
2+
3+
import { HttpTransactionCheckDataSource } from "@/modules/multichain/transaction-check/data/HttpTransactionCheckDataSource";
4+
import { transactionCheckTypes } from "@/modules/multichain/transaction-check/di/transactionCheckTypes";
5+
import { SolanaTransactionCheckLoader } from "@/modules/multichain/transaction-check/loaders/SolanaTransactionCheckLoader";
6+
7+
export const solanaTransactionCheckModuleFactory = () =>
8+
new ContainerModule(({ bind }) => {
9+
bind(transactionCheckTypes.TransactionCheckDataSource).to(
10+
HttpTransactionCheckDataSource,
11+
);
12+
bind(transactionCheckTypes.TransactionCheckLoader).to(
13+
SolanaTransactionCheckLoader,
14+
);
15+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
DeviceModelId,
3+
LoggerPublisherService,
4+
} from "@ledgerhq/device-management-kit";
5+
import { inject, injectable } from "inversify";
6+
import { Codec, exactly, number, oneOf, string } from "purify-ts";
7+
8+
import { configTypes } from "@/config/di/configTypes";
9+
import { pkiTypes } from "@/modules/multichain/pki/di/pkiTypes";
10+
import { type PkiCertificateLoader } from "@/modules/multichain/pki/domain/PkiCertificateLoader";
11+
import { KeyUsage } from "@/modules/multichain/pki/model/KeyUsage";
12+
import { type TransactionCheckDataSource } from "@/modules/multichain/transaction-check/data/TransactionCheckDataSource";
13+
import { transactionCheckTypes } from "@/modules/multichain/transaction-check/di/transactionCheckTypes";
14+
import { type TransactionCheckLoader } from "@/modules/multichain/transaction-check/loaders/TransactionCheckLoader";
15+
import { TransactionCheckPaths } from "@/modules/multichain/transaction-check/utils/constants";
16+
import {
17+
ClearSignContext,
18+
ClearSignContextType,
19+
} from "@/shared/model/ClearSignContext";
20+
21+
export type SolanaTransactionCheckRequest = {
22+
from: string;
23+
rawTx: string;
24+
chain: number;
25+
};
26+
27+
export type SolanaTransactionCheckContextInput = {
28+
deviceModelId: DeviceModelId;
29+
transactionCheck: SolanaTransactionCheckRequest;
30+
};
31+
32+
const SUPPORTED_TYPES: ClearSignContextType[] = [
33+
ClearSignContextType.SOLANA_TRANSACTION_CHECK,
34+
];
35+
36+
const solanaTransactionCheckInputCodec = Codec.interface({
37+
deviceModelId: oneOf([
38+
exactly(DeviceModelId.NANO_X),
39+
exactly(DeviceModelId.NANO_SP),
40+
exactly(DeviceModelId.STAX),
41+
exactly(DeviceModelId.FLEX),
42+
]),
43+
transactionCheck: Codec.interface({
44+
from: string,
45+
rawTx: string,
46+
chain: number,
47+
}),
48+
});
49+
50+
@injectable()
51+
export class SolanaTransactionCheckLoader
52+
implements TransactionCheckLoader<SolanaTransactionCheckContextInput>
53+
{
54+
private readonly logger: LoggerPublisherService;
55+
56+
constructor(
57+
@inject(transactionCheckTypes.TransactionCheckDataSource)
58+
private readonly transactionCheckDataSource: TransactionCheckDataSource,
59+
@inject(pkiTypes.PkiCertificateLoader)
60+
private readonly certificateLoader: PkiCertificateLoader,
61+
@inject(configTypes.ContextModuleLoggerFactory)
62+
loggerFactory: (tag: string) => LoggerPublisherService,
63+
) {
64+
this.logger = loggerFactory("SolanaTransactionCheckLoader");
65+
}
66+
67+
canHandle(
68+
input: unknown,
69+
expectedType: ClearSignContextType[],
70+
): input is SolanaTransactionCheckContextInput {
71+
if (!SUPPORTED_TYPES.every((type) => expectedType.includes(type)))
72+
return false;
73+
return solanaTransactionCheckInputCodec.decode(input).caseOf({
74+
Left: () => false,
75+
Right: ({ transactionCheck: { from, rawTx } }) =>
76+
from.length > 0 && rawTx.length > 0,
77+
});
78+
}
79+
80+
async load(
81+
ctx: SolanaTransactionCheckContextInput,
82+
): Promise<ClearSignContext[]> {
83+
const { from, rawTx, chain } = ctx.transactionCheck;
84+
85+
const txCheck = await this.transactionCheckDataSource.check({
86+
path: TransactionCheckPaths.SOLANA_TRANSACTION,
87+
body: { tx: { from, raw: rawTx }, chain },
88+
});
89+
90+
const context = await txCheck.caseOf<Promise<ClearSignContext>>({
91+
Left: (error) =>
92+
Promise.resolve({
93+
type: ClearSignContextType.ERROR,
94+
error,
95+
}),
96+
Right: async (data) => {
97+
const certificate = await this.certificateLoader.loadCertificate({
98+
keyId: data.publicKeyId,
99+
keyUsage: KeyUsage.TxSimulationSigner,
100+
targetDevice: ctx.deviceModelId,
101+
});
102+
103+
return {
104+
type: ClearSignContextType.SOLANA_TRANSACTION_CHECK,
105+
payload: { descriptor: data.descriptor },
106+
certificate,
107+
};
108+
},
109+
});
110+
111+
const result = [context];
112+
this.logger.debug("load result", { data: { result } });
113+
return result;
114+
}
115+
}

packages/signer/context-module/src/modules/multichain/transaction-check/loaders/TransactionCheckLoader.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { type DeviceModelId } from "@ledgerhq/device-management-kit";
22

33
import { type ContextLoader } from "@/shared/domain/ContextLoader";
44

5+
/**
6+
* Common requirement across all transaction-check loaders: a target device
7+
* model is needed to fetch the right PKI certificate. Other fields (signer
8+
* address, raw tx, chain id) are shaped per chain.
9+
*/
510
export type TransactionCheckInput = {
6-
from: string;
711
deviceModelId: DeviceModelId;
812
};
913

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export enum TransactionCheckPaths {
22
ETHEREUM_TRANSACTION = "/ethereum/scan/tx",
33
ETHEREUM_TYPED_DATA = "/ethereum/scan/eip-712",
4+
SOLANA_TRANSACTION = "/solana/scan/tx",
45
}

0 commit comments

Comments
 (0)