Skip to content
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

🛂 Passkey Authentication #21

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = {
"import/prefer-default-export": "error",
"no-console": "error",
"no-shadow": "off", // @typescript-eslint/no-shadow
"react-native/no-raw-text": ["error", { skip: ["Button"] }],
"react-native/no-raw-text": ["error", { skip: ["Button", "Heading", "SizableText"] }],
"unicorn/filename-case": "off", // use default export name
"unicorn/prefer-top-level-await": "off", // unsupported in react-native
},
Expand Down
36 changes: 36 additions & 0 deletions app/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AntDesign, Entypo, Feather } from "@expo/vector-icons";
import React from "react";
import { Text, YStack, XStack, Button, Heading } from "tamagui";

export default function Card() {
return (
<YStack width="100%" backgroundColor="white" paddingLeft={24} paddingRight={24}>
<Heading fontWeight="700" fontSize={16}>
Balance
</Heading>
<XStack justifyContent="space-between" alignItems="center" marginBottom={16}>
<XStack alignItems="center" gap={16}>
<Text fontSize={32}>$100.00</Text>
<AntDesign name="eyeo" size={24} />
</XStack>
<Entypo name="chevron-thin-down" size={20} />
</XStack>
<XStack alignItems="center" gap={4} marginBottom={24}>
<Feather name="arrow-up-right" size={20} color="#8C8795" />
<Text fontSize={14} fontWeight="500" color="#8C8795">
$12.93 (8.71%) Last 7 days
</Text>
</XStack>

<XStack width="100%" gap={8}>
<Button flex={1}>Add Funds</Button>
<Button flex={1} variant="outlined">
Send
</Button>
</XStack>
<Heading fontWeight="700" fontSize={16}>
My Cards
</Heading>
</YStack>
);
}
49 changes: 49 additions & 0 deletions app/create-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useEffect, useCallback, useState } from "react";
import { Button, YStack, Text } from "tamagui";

import type { Card } from "../pomelo/utils/types";
import { getCards, createCard } from "../utils/pomelo/client";

export default function Cards() {
const [cards, setCards] = useState<Card[]>();
const [x, setX] = useState<string>();
const [loading, setLoading] = useState(true);
async function fetchCards() {
setLoading(true);
try {
const c = await getCards();

setCards(c);
} catch (error) {
setX(error instanceof Error ? error.message : "error");
} finally {
setLoading(false);
}
}
useEffect(() => {
void fetchCards();
}, []);
const handleClick = useCallback(async () => {
const card = await createCard();
setX(JSON.stringify(card));
await fetchCards();
}, []);

return (
<YStack>
<Text>
{loading ? "loading" : ""}
{x}
</Text>
{cards?.map((card) => (
<YStack key={card.id} borderWidth={1} borderColor="black">
<Text>
{card.card_type} {card.provider}
</Text>
<Text>{card.last_four} </Text>
</YStack>
))}
<Button onPress={handleClick}>create</Button>
</YStack>
);
}
47 changes: 47 additions & 0 deletions app/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Feather } from "@expo/vector-icons";
import React from "react";
import { Tabs, SizableText, YStack, XStack } from "tamagui";
import { useDisconnect } from "wagmi";

import Auth from "./auth";
import Card from "./create-card";
import RegisterUser from "./register-user";
import ExaLogo from "../assets/exa-logo.svg";

export default function Home() {
const { disconnect } = useDisconnect();

return (
<YStack display="flex" height="100vh">
<XStack paddingLeft={24} paddingRight={24} paddingTop={24} backgroundColor="white" justifyContent="space-between">
<ExaLogo width={24} height={24} />
<XStack gap={12}>
<Feather name="bell" size={24} color="black" />
<Feather name="settings" onPress={() => disconnect()} size={24} color="black" />
</XStack>
</XStack>
<Tabs defaultValue="card" display="flex" flexDirection="column" flex={1}>
<Tabs.Content value="card" flex={1}>
<Card />
</Tabs.Content>
<Tabs.Content value="auth" flex={1}>
<Auth />
</Tabs.Content>
<Tabs.Content value="user" flex={1}>
<RegisterUser />
</Tabs.Content>
<Tabs.List>
<Tabs.Tab value="card" flexShrink={0} flex={1}>
<SizableText fontSize={10}>Card</SizableText>
</Tabs.Tab>
<Tabs.Tab value="auth" flexShrink={0} flex={1}>
<SizableText fontSize={10}>auth</SizableText>
</Tabs.Tab>
<Tabs.Tab value="user" flexShrink={0} flex={1}>
<SizableText fontSize={10}>User</SizableText>
</Tabs.Tab>
</Tabs.List>
</Tabs>
</YStack>
);
}
172 changes: 9 additions & 163 deletions app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,168 +1,14 @@
import Auditor from "@exactly/protocol/deployments/goerli/Auditor.json";
import MarketUSDC from "@exactly/protocol/deployments/goerli/MarketUSDC.json";
import MarketWETH from "@exactly/protocol/deployments/goerli/MarketWETH.json";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import { TurnkeyClient, createActivityPoller, getWebAuthnAttestation } from "@turnkey/http";
import { deviceName } from "expo-device";
import React, { useCallback } from "react";
import { Button, Spinner, Text, XStack, YStack } from "tamagui";
import { UAParser } from "ua-parser-js";
import { formatEther, getAddress, parseUnits, zeroAddress } from "viem";
import {
useAccount,
useConnect,
useDisconnect,
useReadContract,
useSimulateContract,
useWaitForTransactionReceipt,
useWriteContract,
} from "wagmi";
import React from "react";
import { useAccount } from "wagmi";

import base64URLEncode from "../utils/base64URLEncode";
import { rpId, turnkeyAPIPrivateKey, turnkeyAPIPublicKey, turnkeyOrganizationId } from "../utils/constants";
import generateRandomBuffer from "../utils/generateRandomBuffer";
import handleError from "../utils/handleError";
import Home from "./home";
import Welcome from "./welcome";

export default function Home() {
const {
connect,
isPending: isConnecting,
connectors: [connector],
} = useConnect();
const { address } = useAccount();
const { disconnect } = useDisconnect();
const { data: enterWETHSimulation } = useSimulateContract({
abi: Auditor.abi,
address: getAddress(Auditor.address),
functionName: "enterMarket",
args: [MarketWETH.address],
query: { enabled: !!address },
});
const { data: borrowUSDCSimulation } = useSimulateContract({
abi: MarketUSDC.abi,
address: getAddress(MarketUSDC.address),
functionName: "borrow",
args: [parseUnits("1", 6), address, address],
query: { enabled: !!address },
});
const { data: accountLiquidity } = useReadContract({
abi: [
{
name: "accountLiquidity",
type: "function",
stateMutability: "view",
inputs: [
{ internalType: "address", name: "account", type: "address" },
{ internalType: "contract Market", name: "marketToSimulate", type: "address" },
{ internalType: "uint256", name: "withdrawAmount", type: "uint256" },
],
outputs: [
{ internalType: "uint256", name: "sumCollateral", type: "uint256" },
{ internalType: "uint256", name: "sumDebtPlusEffects", type: "uint256" },
],
},
],
address: getAddress(Auditor.address),
functionName: "accountLiquidity",
args: [address ?? zeroAddress, zeroAddress, 0n],
query: { enabled: !!address },
});
const { writeContract, data: txHash, isPending: isSending } = useWriteContract();
const { isSuccess, isLoading: isWaiting } = useWaitForTransactionReceipt({ hash: txHash });
export default function Index() {
const { isConnecting, isReconnecting, isDisconnected } = useAccount();

const createAccount = useCallback(() => {
const name = `exactly, ${new Date().toISOString()}`;
const challenge = generateRandomBuffer();
getWebAuthnAttestation({
publicKey: {
rp: { id: rpId, name: "exactly" },
user: { id: challenge, name, displayName: name },
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
authenticatorSelection: { requireResidentKey: true, residentKey: "required", userVerification: "required" },
challenge,
},
})
.then(async (attestation) => {
const client = new TurnkeyClient(
{ baseUrl: "https://api.turnkey.com" },
new ApiKeyStamper({ apiPublicKey: turnkeyAPIPublicKey, apiPrivateKey: turnkeyAPIPrivateKey }),
);
const activityPoller = createActivityPoller({ client, requestFn: client.createSubOrganization });
const {
result: { createSubOrganizationResultV4 },
} = await activityPoller({
type: "ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V4",
timestampMs: String(Date.now()),
organizationId: turnkeyOrganizationId,
parameters: {
subOrganizationName: attestation.credentialId,
rootQuorumThreshold: 1,
rootUsers: [
{
apiKeys: [],
userName: "account",
authenticators: [
{
authenticatorName: deviceName ?? new UAParser(navigator.userAgent).getBrowser().name ?? "unknown",
challenge: base64URLEncode(challenge),
attestation,
},
],
},
],
wallet: {
walletName: "default",
accounts: [
{
curve: "CURVE_SECP256K1",
addressFormat: "ADDRESS_FORMAT_ETHEREUM",
pathFormat: "PATH_FORMAT_BIP32",
path: "m/44'/60'/0'/0/0",
},
],
},
},
});
if (!createSubOrganizationResultV4?.wallet?.addresses[0]) throw new Error("sub-org creation failed");
})
.catch(handleError);
}, []);
if (isConnecting || isReconnecting) return "loading";

const connectAccount = useCallback(() => {
if (!connector) throw new Error("no connector");
connect({ connector });
}, [connect, connector]);

const disconnectAccount = useCallback(() => {
disconnect();
}, [disconnect]);

const enterWETH = useCallback(() => {
if (!enterWETHSimulation) throw new Error("no simulation");
writeContract(enterWETHSimulation.request);
}, [enterWETHSimulation, writeContract]);

const borrowUSDC = useCallback(() => {
if (!borrowUSDCSimulation) throw new Error("no simulation");
writeContract(borrowUSDCSimulation.request);
}, [borrowUSDCSimulation, writeContract]);

return (
<XStack flex={1} alignItems="center" space>
<YStack flex={1} alignItems="center" space>
<Text textAlign="center">{txHash && `${txHash} ${isSuccess ? "✅" : ""}`}</Text>
<Text textAlign="center">{accountLiquidity && accountLiquidity.map((v) => formatEther(v)).join(", ")}</Text>
<Button onPress={createAccount}>create account</Button>
<Button disabled={!connector || isConnecting} onPress={address ? disconnectAccount : connectAccount}>
{isConnecting ? <Spinner size="small" /> : address ?? "connect"}
</Button>
<Button disabled={!enterWETHSimulation || isSending || isWaiting} onPress={enterWETH}>
enter WETH market
</Button>
<Button disabled={!borrowUSDCSimulation || isSending || isWaiting} onPress={borrowUSDC}>
borrow 1 USDC
</Button>
</YStack>
</XStack>
);
if (isDisconnected) return <Welcome />;
return <Home />;
}
Loading
Loading