Skip to content

Commit

Permalink
✨ (core) [DSDK-221]: Send apdu through usb transport (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger authored Apr 10, 2024
2 parents daa0729 + a552b04 commit 7c5ba68
Show file tree
Hide file tree
Showing 64 changed files with 1,232 additions and 302 deletions.
15 changes: 9 additions & 6 deletions apps/sample/src/app/client-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
"use client";

import React from "react";
import { Flex, Icons, StyleProvider } from "@ledgerhq/react-ui";
import { Flex, StyleProvider } from "@ledgerhq/react-ui";
import styled, { DefaultTheme } from "styled-components";

import { Header } from "@/components/Header";
import { Sidebar } from "@/components/Sidebar";
import { SdkProvider } from "@/providers/DeviceSdkProvider";
import { SessionProvider } from "@/providers/SessionsProvider";
import { GlobalStyle } from "@/styles/globalstyles";

type ClientRootLayoutProps = {
Expand Down Expand Up @@ -44,11 +45,13 @@ const ClientRootLayout: React.FC<ClientRootLayoutProps> = ({ children }) => {
<GlobalStyle />
<body>
<Root>
<Sidebar />
<PageContainer>
<Header />
{children}
</PageContainer>
<SessionProvider>
<Sidebar />
<PageContainer>
<Header />
{children}
</PageContainer>
</SessionProvider>
</Root>
</body>
</StyleProvider>
Expand Down
38 changes: 33 additions & 5 deletions apps/sample/src/components/ApduView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { useCallback, useState } from "react";
import { Button, Divider, Flex, Grid, Input, Text } from "@ledgerhq/react-ui";
import styled, { DefaultTheme } from "styled-components";

import { useApduForm } from "@/hooks/useApduForm";
import { useSdk } from "@/providers/DeviceSdkProvider";
import { useSessionContext } from "@/providers/SessionsProvider";

const Root = styled(Flex).attrs({ mx: 15, mt: 10, mb: 5 })`
flex-direction: column;
Expand Down Expand Up @@ -54,7 +57,27 @@ const InputContainer = styled(Flex).attrs({ mx: 8, mb: 4 })`
const inputContainerProps = { style: { borderRadius: 4 } };

export const ApduView: React.FC = () => {
const { apduFormValues, setApduFormValue, apdu } = useApduForm();
const { apduFormValues, setApduFormValue, getRawApdu } = useApduForm();
const [loading, setLoading] = useState(false);
const sdk = useSdk();
const {
state: { selectedId: selectedSessionId },
} = useSessionContext();
const onSubmit = useCallback(
async (values: typeof apduFormValues) => {
setLoading(true);
try {
await sdk.sendApdu({
sessionId: selectedSessionId!,
apdu: getRawApdu(values),
});
setLoading(false);
} catch (error) {
setLoading(false);
}
},
[getRawApdu, sdk, selectedSessionId],
);
return (
<Root>
<FormContainer>
Expand All @@ -69,11 +92,11 @@ export const ApduView: React.FC = () => {
Class instruction
</Text>
<Input
name="instruction"
name="instructionClass"
containerProps={inputContainerProps}
value={apduFormValues.classInstruction}
value={apduFormValues.instructionClass}
onChange={(value) =>
setApduFormValue("classInstruction", value)
setApduFormValue("instructionClass", value)
}
/>
</InputContainer>
Expand Down Expand Up @@ -118,6 +141,7 @@ export const ApduView: React.FC = () => {
</Text>
<Input
name="data"
placeholder="<NO DATA>"
containerProps={inputContainerProps}
value={apduFormValues.data}
onChange={(value) => setApduFormValue("data", value)}
Expand All @@ -129,6 +153,7 @@ export const ApduView: React.FC = () => {
</Text>
<Input
name="dataLength"
disabled
containerProps={inputContainerProps}
value={apduFormValues.dataLength}
onChange={(value) => setApduFormValue("dataLength", value)}
Expand All @@ -138,7 +163,10 @@ export const ApduView: React.FC = () => {
</Form>
<Divider my={4} />
<FormFooter my={8}>
<FormFooterButton onClick={() => console.log(apdu)}>
<FormFooterButton
onClick={() => onSubmit(apduFormValues)}
disabled={loading}
>
<Text color="neutral.c00">Send APDU</Text>
</FormFooterButton>
</FormFooter>
Expand Down
23 changes: 5 additions & 18 deletions apps/sample/src/components/Device/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import { ConnectionType, DeviceModelId } from "@ledgerhq/device-sdk-core";
import { Box, Flex, Icons, Text } from "@ledgerhq/react-ui";
import styled, { DefaultTheme } from "styled-components";

Expand All @@ -25,38 +26,24 @@ export enum DeviceStatus {
NOT_CONNECTED = "Not Connected",
}

export enum DeviceType {
USB = "USB",
BLE = "BLE",
}

export enum DeviceModel {
STAX = "Stax",
LNX = "LNX",
}

// These props are subject to change.
type DeviceProps = {
name: string;
type: DeviceType;
model: DeviceModel;
type: ConnectionType;
model: DeviceModelId;
status?: DeviceStatus;
};

export const Device: React.FC<DeviceProps> = ({
name,
status,
status = DeviceStatus.AVAILABLE,
type,
model,
}) => {
return (
<Root>
<IconContainer>
{model === DeviceModel.STAX ? (
<Icons.Stax size="S" />
) : (
<Icons.Nano size="S" />
)}
{model === "stax" ? <Icons.Stax size="S" /> : <Icons.Nano size="S" />}
</IconContainer>
<Box flex={1}>
<Text variant="body">{name}</Text>
Expand Down
13 changes: 11 additions & 2 deletions apps/sample/src/components/MainView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Image from "next/image";
import styled, { DefaultTheme } from "styled-components";

import { useSdk } from "@/providers/DeviceSdkProvider";
import { useSessionContext } from "@/providers/SessionsProvider";

const Root = styled(Flex)`
flex: 1;
Expand All @@ -23,6 +24,7 @@ const NanoLogo = styled(Image).attrs({ mb: 8 })`

export const MainView: React.FC = () => {
const sdk = useSdk();
const { dispatch } = useSessionContext();
const [discoveredDevice, setDiscoveredDevice] =
useState<null | DiscoveredDevice>(null);

Expand Down Expand Up @@ -50,10 +52,17 @@ export const MainView: React.FC = () => {
if (discoveredDevice) {
sdk
.connect({ deviceId: discoveredDevice.id })
.then((connectedDevice) => {
.then((sessionId) => {
console.log(
`🦖 Response from connect: ${JSON.stringify(connectedDevice)} 🎉`,
`🦖 Response from connect: ${JSON.stringify(sessionId)} 🎉`,
);
dispatch({
type: "add_session",
payload: {
sessionId,
connectedDevice: sdk.getConnectedDevice({ sessionId }),
},
});
})
.catch((error) => {
console.error(`Error from connection or get-version`, error);
Expand Down
26 changes: 14 additions & 12 deletions apps/sample/src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@ import { Box, Flex, Icons, Link, Text } from "@ledgerhq/react-ui";
import { useRouter } from "next/navigation";
import styled, { DefaultTheme } from "styled-components";

import {
Device,
DeviceModel,
DeviceStatus,
DeviceType,
} from "@/components/Device";
import { Device } from "@/components/Device";
import { Menu } from "@/components/Menu";
import { useSdk } from "@/providers/DeviceSdkProvider";
import { useSessionContext } from "@/providers/SessionsProvider";

const Root = styled(Flex).attrs({ py: 8, px: 6 })`
flex-direction: column;
Expand Down Expand Up @@ -47,6 +43,9 @@ const VersionText = styled(Text)`
export const Sidebar: React.FC = () => {
const [version, setVersion] = useState("");
const sdk = useSdk();
const {
state: { deviceById },
} = useSessionContext();

useEffect(() => {
sdk
Expand Down Expand Up @@ -76,12 +75,15 @@ export const Sidebar: React.FC = () => {
</Subtitle>

<Subtitle variant={"tiny"}>Device</Subtitle>
<Device
name="Ledger Nano X 9EAB"
model={DeviceModel.LNX}
status={DeviceStatus.LOCKED}
type={DeviceType.BLE}
/>

{Object.entries(deviceById).map(([sessionId, device]) => (
<Device
key={sessionId}
name={device.name}
model={device.modelId}
type={device.type}
/>
))}

<MenuContainer>
<Subtitle variant={"tiny"}>Menu</Subtitle>
Expand Down
50 changes: 28 additions & 22 deletions apps/sample/src/hooks/useApduForm.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";

type ApduFormValues = {
classInstruction: string;
instructionClass: string;
instructionMethod: string;
firstParameter: string;
secondParameter: string;
Expand All @@ -11,36 +11,42 @@ type ApduFormValues = {

export function useApduForm() {
const [values, setValues] = useState<ApduFormValues>({
classInstruction: "",
instructionMethod: "",
firstParameter: "",
secondParameter: "",
instructionClass: "e0",
instructionMethod: "01",
firstParameter: "00",
secondParameter: "00",
dataLength: "00",
data: "",
dataLength: "",
});
const [apdu, setApdu] = useState<Uint8Array>(Uint8Array.from([]));

const setValue = useCallback((field: keyof ApduFormValues, value: string) => {
setValues((prev) => ({ ...prev, [field]: value }));
const newValues = { [field]: value };
if (field === "data") {
newValues.dataLength = Math.floor(value.length / 2).toString(16);
}
setValues((prev) => ({ ...prev, ...newValues }));
}, []);

useEffect(() => {
const newApdu = Object.values(values).reduce(
(acc, curr) => [
...acc,
...chunkString(curr.replace(/\s/g, ""))
.map((char) => Number(`0x${char}`))
.filter((nbr) => !Number.isNaN(nbr)),
],
[] as number[],
);
setApdu(Uint8Array.from(newApdu));
}, [values]);
const getRawApdu = useCallback(
(formValues: ApduFormValues): Uint8Array =>
new Uint8Array(
Object.values(formValues).reduce(
(acc, curr) => [
...acc,
...chunkString(curr.replace(/\s/g, ""))
.map((char) => Number(`0x${char}`))
.filter((nbr) => !Number.isNaN(nbr)),
],
[] as number[],
),
),
[],
);

return {
apduFormValues: values,
setApduFormValue: setValue,
apdu,
getRawApdu,
};
}

Expand Down
2 changes: 1 addition & 1 deletion apps/sample/src/providers/DeviceSdkProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export const SdkProvider: React.FC<Props> = ({ children }) => {
return <SdkContext.Provider value={sdk}>{children}</SdkContext.Provider>;
};

export const useSdk = () => {
export const useSdk = (): DeviceSdk => {
return useContext(SdkContext);
};
34 changes: 34 additions & 0 deletions apps/sample/src/providers/SessionsProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Context, createContext, useContext, useReducer } from "react";

import {
AddSessionAction,
SessionsInitialState,
sessionsReducer,
SessionsState,
} from "@/reducers/sessions";

type SessionContextType = {
state: SessionsState;
dispatch: (value: AddSessionAction) => void;
};

const SessionContext: Context<SessionContextType> =
createContext<SessionContextType>({
state: SessionsInitialState,
dispatch: (_value: AddSessionAction) => {},
});

export const SessionProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [state, dispatch] = useReducer(sessionsReducer, SessionsInitialState);

return (
<SessionContext.Provider value={{ state, dispatch }}>
{children}
</SessionContext.Provider>
);
};

export const useSessionContext = () =>
useContext<SessionContextType>(SessionContext);
Loading

0 comments on commit 7c5ba68

Please sign in to comment.