Skip to content

Commit 756a03d

Browse files
✨ (solana-signer): Add solana swap transaction signer
1 parent e7cdf7e commit 756a03d

File tree

16 files changed

+1932
-2
lines changed

16 files changed

+1932
-2
lines changed

.changeset/slimy-walls-talk.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 swap transaction signer

apps/sample/src/components/SignerSolanaView/index.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import {
2323
type SignTransactionDAOutput,
2424
SolanaToolsBuilder,
2525
} from "@ledgerhq/device-signer-kit-solana";
26+
import {
27+
type SwapTransactionSignerDAError,
28+
type SwapTransactionSignerDAIntermediateValue,
29+
type SwapTransactionSignerDAOutput,
30+
} from "@ledgerhq/device-signer-kit-solana/api/app-binder/SwapTransactionSignerDeviceActionTypes.js";
2631

2732
import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
2833
import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
@@ -82,7 +87,7 @@ export const SignerSolanaView: React.FC<{ sessionId: string }> = ({
8287
GetAddressDAIntermediateValue
8388
>,
8489
{
85-
title: "Sign Transaction",
90+
title: "Sign transaction",
8691
description:
8792
"Perform all the actions necessary to sign a Solana transaction with the device",
8893
executeDeviceAction: ({ derivationPath, transaction }) => {
@@ -146,7 +151,7 @@ export const SignerSolanaView: React.FC<{ sessionId: string }> = ({
146151
GetAppConfigurationDAIntermediateValue
147152
>,
148153
{
149-
title: "Generate Transaction",
154+
title: "Generate transaction",
150155
description:
151156
"Perform all the actions necessary to generate a transaction to test the Solana signer",
152157
executeDeviceAction: ({ derivationPath }) => {
@@ -166,6 +171,32 @@ export const SignerSolanaView: React.FC<{ sessionId: string }> = ({
166171
GenerateTransactionDAError,
167172
GenerateTransactionDAIntermediateValue
168173
>,
174+
{
175+
title: "Swap transaction signer",
176+
description:
177+
"Perform all the actions necessary to swap a transaction signer",
178+
executeDeviceAction: ({ derivationPath, serialisedTransaction }) => {
179+
return solanaTools.swapTransactionSigner(
180+
derivationPath,
181+
serialisedTransaction,
182+
);
183+
},
184+
initialValues: {
185+
derivationPath: DEFAULT_DERIVATION_PATH,
186+
serialisedTransaction: "",
187+
skipOpenApp: false,
188+
},
189+
deviceModelId,
190+
} satisfies DeviceActionProps<
191+
SwapTransactionSignerDAOutput,
192+
{
193+
derivationPath: string;
194+
serialisedTransaction: string;
195+
skipOpenApp: boolean;
196+
},
197+
SwapTransactionSignerDAError,
198+
SwapTransactionSignerDAIntermediateValue
199+
>,
169200
],
170201
[deviceModelId, solanaTools, signer],
171202
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { type ContextModule } from "@ledgerhq/context-module";
2+
import {
3+
type DeviceActionState,
4+
type ExecuteDeviceActionReturnType,
5+
type OpenAppDAError,
6+
type OpenAppDARequiredInteraction,
7+
type SendCommandInAppDAError,
8+
type UserInteractionRequired,
9+
} from "@ledgerhq/device-management-kit";
10+
11+
import { type PublicKey } from "@api/model/PublicKey";
12+
import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors";
13+
14+
export type SwapTransactionSignerDAOutput = string;
15+
16+
export type SwapTransactionSignerDAInput = {
17+
readonly derivationPath: string;
18+
readonly serialisedTransaction: string;
19+
readonly skipOpenApp: boolean;
20+
readonly contextModule: ContextModule;
21+
};
22+
23+
export type SwapTransactionSignerDAError =
24+
| OpenAppDAError
25+
| SendCommandInAppDAError<SolanaAppErrorCodes>;
26+
27+
type SwapTransactionSignerDARequiredInteraction =
28+
| UserInteractionRequired
29+
| OpenAppDARequiredInteraction;
30+
31+
export type SwapTransactionSignerDAIntermediateValue = {
32+
requiredUserInteraction: SwapTransactionSignerDARequiredInteraction;
33+
};
34+
35+
export type SwapTransactionSignerDAState = DeviceActionState<
36+
SwapTransactionSignerDAOutput,
37+
SwapTransactionSignerDAError,
38+
SwapTransactionSignerDAIntermediateValue
39+
>;
40+
41+
export type SwapTransactionSignerDAInternalState = {
42+
readonly error: SwapTransactionSignerDAError | null;
43+
readonly publicKey: PublicKey | null;
44+
readonly serialisedTransaction: string | null;
45+
};
46+
47+
export type SwapTransactionSignerDAReturnType = ExecuteDeviceActionReturnType<
48+
SwapTransactionSignerDAOutput,
49+
SwapTransactionSignerDAError,
50+
SwapTransactionSignerDAIntermediateValue
51+
>;

packages/signer/signer-solana/src/internal/DefaultSolanaTools.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,21 @@ describe("DefaultSolanaTools", () => {
6060
solanaTools.generateTransaction("derivationPath");
6161
expect(dmk.executeDeviceAction).toHaveBeenCalled();
6262
});
63+
64+
it("should call swapTransaction", () => {
65+
const dmk = {
66+
executeDeviceAction: vi.fn(),
67+
} as unknown as DeviceManagementKit;
68+
const sessionId = {} as DeviceSessionId;
69+
const solanaTools = new DefaultSolanaTools({
70+
dmk,
71+
sessionId,
72+
contextModule: contextModuleStub,
73+
});
74+
solanaTools.swapTransactionSigner(
75+
"derivationPath",
76+
"serialisedTransaction",
77+
);
78+
expect(dmk.executeDeviceAction).toHaveBeenCalled();
79+
});
6380
});

packages/signer/signer-solana/src/internal/DefaultSolanaTools.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { type Container } from "inversify";
88
import { type GenerateTransactionDAReturnType } from "@api/app-binder/GenerateTransactionDeviceActionTypes";
99
import { type GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTypes";
1010
import { type GetAppConfigurationDAReturnType } from "@api/app-binder/GetAppConfigurationDeviceActionTypes";
11+
import { type SwapTransactionSignerDAReturnType } from "@api/app-binder/SwapTransactionSignerDeviceActionTypes";
1112
import { type AddressOptions } from "@api/model/AddressOption";
1213
import { type SolanaTools } from "@api/SolanaTools";
1314

1415
import { type GetAddressUseCase } from "./use-cases/address/GetAddressUseCase";
1516
import { type GetAppConfigurationUseCase } from "./use-cases/app-configuration/GetAppConfigurationUseCase";
1617
import { useCasesTypes } from "./use-cases/di/useCasesTypes";
1718
import { type GenerateTransactionUseCase } from "./use-cases/generateTransaction/GenerateTransactionUseCase";
19+
import { type SwapTransactionSignerUseCase } from "./use-cases/swap-transaction-signer/SwapTransactionSignerUseCase";
1820
import { makeContainer } from "./di";
1921

2022
export type DefaultSolanaToolsConstructorArgs = {
@@ -40,6 +42,17 @@ export class DefaultSolanaTools implements SolanaTools {
4042
.execute(derivationPath);
4143
}
4244

45+
swapTransactionSigner(
46+
derivationPath: string,
47+
serialisedTransaction: string,
48+
): SwapTransactionSignerDAReturnType {
49+
return this._container
50+
.get<SwapTransactionSignerUseCase>(
51+
useCasesTypes.SwapTransactionSignerUseCase,
52+
)
53+
.execute(derivationPath, serialisedTransaction);
54+
}
55+
4356
getAddress(
4457
derivationPath: string,
4558
options?: AddressOptions,

packages/signer/signer-solana/src/internal/app-binder/SolanaAppBinder.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { GetAppConfigurationCommand } from "./command/GetAppConfigurationCommand
3535
import { GetPubKeyCommand } from "./command/GetPubKeyCommand";
3636
import { GenerateTransactionDeviceAction } from "./device-action/GenerateTransactionDeviceAction";
3737
import { SignTransactionDeviceAction } from "./device-action/SignTransactionDeviceAction";
38+
import { SwapTransactionSignerDeviceAction } from "./device-action/SwapTransactionSignerDeviceAction";
3839
import { SolanaAppBinder } from "./SolanaAppBinder";
3940

4041
describe("SolanaAppBinder", () => {
@@ -515,4 +516,94 @@ describe("SolanaAppBinder", () => {
515516
});
516517
});
517518
});
519+
520+
describe("swapTransactionSigner", () => {
521+
it("should return the swapped serialized transaction", () =>
522+
new Promise<void>((resolve, reject) => {
523+
// given
524+
const swappedSerializedTx = "SWAPPED_BASE64";
525+
526+
vi.spyOn(mockedDmk, "executeDeviceAction").mockReturnValue({
527+
observable: from([
528+
{
529+
status: DeviceActionStatus.Completed,
530+
output: swappedSerializedTx,
531+
} as DeviceActionState<
532+
unknown,
533+
DmkError,
534+
DeviceActionIntermediateValue
535+
>,
536+
]),
537+
cancel: vi.fn(),
538+
});
539+
540+
// when
541+
const appBinder = new SolanaAppBinder(
542+
mockedDmk,
543+
"sessionId",
544+
contextModuleStub,
545+
);
546+
const { observable } = appBinder.SwapTransactionSigner({
547+
derivationPath: "44'/501'/0'/0'",
548+
serialisedTransaction: "INPUT_BASE64",
549+
skipOpenApp: false,
550+
});
551+
552+
// then
553+
const states: DeviceActionState<
554+
unknown,
555+
DmkError,
556+
DeviceActionIntermediateValue
557+
>[] = [];
558+
observable.subscribe({
559+
next: (state) => states.push(state),
560+
error: (err) => reject(err),
561+
complete: () => {
562+
try {
563+
expect(states).toEqual([
564+
{
565+
status: DeviceActionStatus.Completed,
566+
output: swappedSerializedTx,
567+
},
568+
]);
569+
resolve();
570+
} catch (err) {
571+
reject(err as Error);
572+
}
573+
},
574+
});
575+
}));
576+
577+
it("should call executeDeviceAction with the correct params", () => {
578+
// given
579+
const derivationPath = "44'/501'/0'/0'";
580+
const serialisedTransaction = "INPUT_BASE64";
581+
const skipOpenApp = true;
582+
583+
// when
584+
const appBinder = new SolanaAppBinder(
585+
mockedDmk,
586+
"sessionId",
587+
contextModuleStub,
588+
);
589+
appBinder.SwapTransactionSigner({
590+
derivationPath,
591+
serialisedTransaction,
592+
skipOpenApp,
593+
});
594+
595+
// then
596+
expect(mockedDmk.executeDeviceAction).toHaveBeenCalledWith({
597+
sessionId: "sessionId",
598+
deviceAction: new SwapTransactionSignerDeviceAction({
599+
input: {
600+
derivationPath,
601+
serialisedTransaction,
602+
skipOpenApp,
603+
contextModule: contextModuleStub,
604+
},
605+
}),
606+
});
607+
});
608+
});
518609
});

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTy
1313
import { GetAppConfigurationDAReturnType } from "@api/app-binder/GetAppConfigurationDeviceActionTypes";
1414
import { SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes";
1515
import { SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes";
16+
import { SwapTransactionSignerDAReturnType } from "@api/app-binder/SwapTransactionSignerDeviceActionTypes";
1617
import { SolanaTransactionOptionalConfig } from "@api/model/SolanaTransactionOptionalConfig";
1718
import { Transaction } from "@api/model/Transaction";
1819
import { SendSignMessageTask } from "@internal/app-binder/task/SendSignMessageTask";
@@ -22,6 +23,7 @@ import { GetAppConfigurationCommand } from "./command/GetAppConfigurationCommand
2223
import { GetPubKeyCommand } from "./command/GetPubKeyCommand";
2324
import { GenerateTransactionDeviceAction } from "./device-action/GenerateTransactionDeviceAction";
2425
import { SignTransactionDeviceAction } from "./device-action/SignTransactionDeviceAction";
26+
import { SwapTransactionSignerDeviceAction } from "./device-action/SwapTransactionSignerDeviceAction";
2527

2628
@injectable()
2729
export class SolanaAppBinder {
@@ -85,6 +87,24 @@ export class SolanaAppBinder {
8587
});
8688
}
8789

90+
SwapTransactionSigner(args: {
91+
derivationPath: string;
92+
serialisedTransaction: string;
93+
skipOpenApp: boolean;
94+
}): SwapTransactionSignerDAReturnType {
95+
return this.dmk.executeDeviceAction({
96+
sessionId: this.sessionId,
97+
deviceAction: new SwapTransactionSignerDeviceAction({
98+
input: {
99+
derivationPath: args.derivationPath,
100+
serialisedTransaction: args.serialisedTransaction,
101+
skipOpenApp: args.skipOpenApp,
102+
contextModule: this.contextModule,
103+
},
104+
}),
105+
});
106+
}
107+
88108
signMessage(args: {
89109
derivationPath: string;
90110
message: string;

0 commit comments

Comments
 (0)