Skip to content

Commit

Permalink
✨ (signer-solana) [DSDK-554]: Add GetAddressUseCase (#451)
Browse files Browse the repository at this point in the history
  • Loading branch information
aussedatlo authored Oct 29, 2024
2 parents 1c8113b + c11b4b8 commit 238ed69
Show file tree
Hide file tree
Showing 23 changed files with 389 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-badgers-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-sdk-sample": minor
---

Add GetAddress Solana Signer use case
5 changes: 5 additions & 0 deletions .changeset/old-impalas-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-solana": minor
---

Add GetAddressUseCase
1 change: 1 addition & 0 deletions apps/sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@ledgerhq/device-management-kit": "workspace:*",
"@ledgerhq/device-management-kit-flipper-plugin-client": "workspace:*",
"@ledgerhq/device-signer-kit-ethereum": "workspace:*",
"@ledgerhq/device-signer-kit-solana": "workspace:*",
"@ledgerhq/device-sdk-transport-mock": "workspace:*",
"@ledgerhq/react-ui": "^0.16.2",
"@sentry/nextjs": "^8.32.0",
Expand Down
11 changes: 11 additions & 0 deletions apps/sample/src/app/keyring/solana/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";
import React from "react";

import { SessionIdWrapper } from "@/components/SessionIdWrapper";
import { SignerSolanaView } from "@/components/SignerSolanaView";

const Signer: React.FC = () => {
return <SessionIdWrapper ChildComponent={SignerSolanaView} />;
};

export default Signer;
5 changes: 5 additions & 0 deletions apps/sample/src/components/KeyringView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const SUPPORTED_KEYRINGS = [
description: "Access Bitcoin keyring functionality",
icon: <CryptoIcons.BTC size={80} />,
},
{
title: "Solana",
description: "Access Solana keyring functionality",
icon: <CryptoIcons.SOL size={80} />,
},
];

export const KeyringView = () => {
Expand Down
58 changes: 58 additions & 0 deletions apps/sample/src/components/SignerSolanaView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useMemo } from "react";
import {
GetAddressDAError,
GetAddressDAIntermediateValue,
GetAddressDAOutput,
SignerSolanaBuilder,
} from "@ledgerhq/device-signer-kit-solana";

import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
import { DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
import { useSdk } from "@/providers/DeviceSdkProvider";

const DEFAULT_DERIVATION_PATH = "44'/501'";

export const SignerSolanaView: React.FC<{ sessionId: string }> = ({
sessionId,
}) => {
const sdk = useSdk();
const signer = new SignerSolanaBuilder({ sdk, sessionId }).build();

const deviceModelId = sdk.getConnectedDevice({
sessionId,
}).modelId;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const deviceActions: DeviceActionProps<any, any, any, any>[] = useMemo(
() => [
{
title: "Get address",
description:
"Perform all the actions necessary to get a Solana address from the device",
executeDeviceAction: ({ derivationPath, checkOnDevice }) => {
return signer.getAddress(derivationPath, {
checkOnDevice,
});
},
initialValues: {
derivationPath: DEFAULT_DERIVATION_PATH,
checkOnDevice: false,
},
deviceModelId,
} satisfies DeviceActionProps<
GetAddressDAOutput,
{
derivationPath: string;
checkOnDevice?: boolean;
},
GetAddressDAError,
GetAddressDAIntermediateValue
>,
],
[deviceModelId, signer],
);

return (
<DeviceActionsList title="Solana Signer" deviceActions={deviceActions} />
);
};
3 changes: 2 additions & 1 deletion packages/signer/signer-solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"test:coverage": "pnpm test -- --coverage"
},
"dependencies": {
"@ledgerhq/signer-utils": "workspace:*",
"bs58": "^6.0.0",
"inversify": "^6.0.3",
"inversify-logger-middleware": "^3.1.0",
Expand All @@ -48,8 +49,8 @@
"@ledgerhq/eslint-config-dsdk": "workspace:*",
"@ledgerhq/jest-config-dsdk": "workspace:*",
"@ledgerhq/prettier-config-dsdk": "workspace:*",
"@ledgerhq/signer-utils": "workspace:*",
"@ledgerhq/tsconfig-dsdk": "workspace:*",
"rxjs": "^7.8.1",
"ts-node": "^10.9.2"
},
"peerDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions packages/signer/signer-solana/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type {
GetAddressDAError,
GetAddressDAIntermediateValue,
GetAddressDAOutput,
GetAddressDAReturnType,
} from "@api/app-binder/GetAddressDeviceActionTypes";
export type { SignerSolana } from "@api/SignerSolana";
export { SignerSolanaBuilder } from "@api/SignerSolanaBuilder";
2 changes: 2 additions & 0 deletions packages/signer/signer-solana/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// inversify requirement
import "reflect-metadata";

export * from "@api/index";
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Transaction } from "@api/model/Transaction";
import { TransactionOptions } from "@api/model/TransactionOptions";
import { SignerSolana } from "@api/SignerSolana";

import { GetAddressUseCase } from "./use-cases/address/GetAddressUseCase";
import { useCasesTypes } from "./use-cases/di/useCasesTypes";
import { makeContainer } from "./di";

export type DefaultSignerSolanaConstructorArgs = {
Expand All @@ -22,8 +24,6 @@ export class DefaultSignerSolana implements SignerSolana {

constructor({ sdk, sessionId }: DefaultSignerSolanaConstructorArgs) {
this._container = makeContainer({ sdk, sessionId });
// FIXME: avoid lint error for now
console.log(this._container);
}

signTransaction(
Expand All @@ -42,10 +42,12 @@ export class DefaultSignerSolana implements SignerSolana {
}

getAddress(
_derivationPath: string,
_options?: AddressOptions,
derivationPath: string,
options?: AddressOptions,
): GetAddressDAReturnType {
return {} as GetAddressDAReturnType;
return this._container
.get<GetAddressUseCase>(useCasesTypes.GetAddressUseCase)
.execute(derivationPath, options);
}

getAppConfiguration(): GetAppConfigurationDAReturnType {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,147 @@
import { DeviceSdk, DeviceSessionId } from "@ledgerhq/device-management-kit";
import {
DeviceActionState,
DeviceActionStatus,
DeviceSdk,
DeviceSessionId,
SendCommandInAppDeviceAction,
UserInteractionRequired,
} from "@ledgerhq/device-management-kit";
import { from } from "rxjs";

import {
GetAddressDAError,
GetAddressDAIntermediateValue,
GetAddressDAOutput,
} from "@api/index";

import { GetPubKeyCommand } from "./command/GetPubKeyCommand";
import { SolanaAppBinder } from "./SolanaAppBinder";

describe("SolanaAppBinder", () => {
const mockedSdk: DeviceSdk = {
sendCommand: jest.fn(),
executeDeviceAction: jest.fn(),
} as unknown as DeviceSdk;

beforeEach(() => {
jest.clearAllMocks();
});

it("should be defined", () => {
const binder = new SolanaAppBinder({} as DeviceSdk, {} as DeviceSessionId);
expect(binder).toBeDefined();
});

describe("getAddress", () => {
it("should return the address", (done) => {
// GIVEN
const address = "D2PPQSYFe83nDzk96FqGumVU8JA7J8vj2Rhjc2oXzEi5";

jest.spyOn(mockedSdk, "executeDeviceAction").mockReturnValue({
observable: from([
{
status: DeviceActionStatus.Completed,
output: address,
} as DeviceActionState<
GetAddressDAOutput,
GetAddressDAError,
GetAddressDAIntermediateValue
>,
]),
cancel: jest.fn(),
});

// WHEN
const appBinder = new SolanaAppBinder(mockedSdk, "sessionId");
const { observable } = appBinder.getAddress({
derivationPath: "44'/501'",
checkOnDevice: false,
});

// THEN
const states: DeviceActionState<
GetAddressDAOutput,
GetAddressDAError,
GetAddressDAIntermediateValue
>[] = [];
observable.subscribe({
next: (state) => {
states.push(state);
},
error: (err) => {
done(err);
},
complete: () => {
try {
expect(states).toEqual([
{
status: DeviceActionStatus.Completed,
output: address,
},
]);
done();
} catch (err) {
done(err);
}
},
});
});

describe("calls of executeDeviceAction with the correct params", () => {
const baseParams = {
derivationPath: "44'/60'/3'/2/1",
returnChainCode: false,
};

it("when checkOnDevice is true: UserInteractionRequired.VerifyAddress", () => {
// GIVEN
const checkOnDevice = true;
const params = {
...baseParams,
checkOnDevice,
};

// WHEN
const appBinder = new SolanaAppBinder(mockedSdk, "sessionId");
appBinder.getAddress(params);

// THEN
expect(mockedSdk.executeDeviceAction).toHaveBeenCalledWith({
sessionId: "sessionId",
deviceAction: new SendCommandInAppDeviceAction({
input: {
command: new GetPubKeyCommand(params),
appName: "Solana",
requiredUserInteraction: UserInteractionRequired.VerifyAddress,
},
}),
});
});

it("when checkOnDevice is false: UserInteractionRequired.None", () => {
// GIVEN
const checkOnDevice = false;
const params = {
...baseParams,
checkOnDevice,
};

// WHEN
const appBinder = new SolanaAppBinder(mockedSdk, "sessionId");
appBinder.getAddress(params);

// THEN
expect(mockedSdk.executeDeviceAction).toHaveBeenCalledWith({
sessionId: "sessionId",
deviceAction: new SendCommandInAppDeviceAction({
input: {
command: new GetPubKeyCommand(params),
appName: "Solana",
requiredUserInteraction: UserInteractionRequired.None,
},
}),
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
DeviceSdk,
type DeviceSessionId,
SendCommandInAppDeviceAction,
UserInteractionRequired,
} from "@ledgerhq/device-management-kit";
import { inject } from "inversify";
import { inject, injectable } from "inversify";

import { GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTypes";
import { GetAppConfigurationDAReturnType } from "@api/app-binder/GetAppConfigurationDeviceActionTypes";
Expand All @@ -11,21 +13,31 @@ import { SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDevi
import { Transaction } from "@api/model/Transaction";
import { externalTypes } from "@internal/externalTypes";

import { GetPubKeyCommand } from "./command/GetPubKeyCommand";

@injectable()
export class SolanaAppBinder {
constructor(
@inject(externalTypes.Sdk) private _sdk: DeviceSdk,
@inject(externalTypes.SessionId) private _sessionId: DeviceSessionId,
) {
// FIXME: avoid lint error for now
console.log(this._sdk);
console.log(this._sessionId);
}
@inject(externalTypes.Sdk) private sdk: DeviceSdk,
@inject(externalTypes.SessionId) private sessionId: DeviceSessionId,
) {}

getAddress(_args: {
getAddress(args: {
derivationPath: string;
checkOnDevice: boolean;
}): GetAddressDAReturnType {
return {} as GetAddressDAReturnType;
return this.sdk.executeDeviceAction({
sessionId: this.sessionId,
deviceAction: new SendCommandInAppDeviceAction({
input: {
command: new GetPubKeyCommand(args),
appName: "Solana",
requiredUserInteraction: args.checkOnDevice
? UserInteractionRequired.VerifyAddress
: UserInteractionRequired.None,
},
}),
});
}

signTransaction(_args: {
Expand Down
3 changes: 2 additions & 1 deletion packages/signer/signer-solana/src/internal/di.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Container } from "inversify";

// import { makeLoggerMiddleware } from "inversify-logger-middleware";
import { appBinderModuleFactory } from "./app-binder/di/appBinderModule";
import { useCasesModuleFactory } from "./use-cases/di/useCasesModule";
import { externalTypes } from "./externalTypes";

// Uncomment this line to enable the logger middleware
Expand All @@ -23,7 +24,7 @@ export const makeContainer = ({ sdk, sessionId }: MakeContainerProps) => {
.bind<DeviceSessionId>(externalTypes.SessionId)
.toConstantValue(sessionId);

container.load(appBinderModuleFactory());
container.load(appBinderModuleFactory(), useCasesModuleFactory());

return container;
};
Empty file.
Loading

0 comments on commit 238ed69

Please sign in to comment.