Skip to content

Commit

Permalink
✨ alchemy: implement wagmi connector
Browse files Browse the repository at this point in the history
  • Loading branch information
cruzdanilo committed Nov 23, 2023
1 parent 914faa6 commit d708888
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 141 deletions.
15 changes: 6 additions & 9 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ import * as Sentry from "sentry-expo";
import { TamaguiProvider } from "tamagui";
import { TextEncoder } from "text-encoding";
import { WagmiConfig, configureChains, createConfig } from "wagmi";
import { goerli } from "wagmi/chains";
import { alchemyProvider } from "wagmi/providers/alchemy";
import { publicProvider } from "wagmi/providers/public";

import metadata from "../package.json";
import tamaguiConfig from "../tamagui.config";
import AlchemyConnector from "../utils/AlchemyConnector";
import { alchemyAPIKey, chain } from "../utils/constants";

export { ErrorBoundary } from "expo-router";

export const unstable_settings = { initialRouteName: "/" };

SplashScreen.preventAutoHideAsync().catch(() => {});
void SplashScreen.preventAutoHideAsync(); // eslint-disable-line no-void -- android bug

Sentry.init({
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
Expand All @@ -35,14 +36,10 @@ Sentry.init({
});

const { publicClient, webSocketPublicClient } = configureChains(
[goerli],
[
process.env.EXPO_PUBLIC_ALCHEMY_API_KEY
? alchemyProvider({ apiKey: process.env.EXPO_PUBLIC_ALCHEMY_API_KEY })
: publicProvider(),
],
[chain],
[alchemyAPIKey ? alchemyProvider({ apiKey: alchemyAPIKey }) : publicProvider()],
);
const wagmiConfig = createConfig({ autoConnect: true, publicClient, webSocketPublicClient });
const wagmiConfig = createConfig({ connectors: [new AlchemyConnector()], publicClient, webSocketPublicClient });

export default function RootLayout() {
const [loaded, error] = useFonts({
Expand Down
123 changes: 32 additions & 91 deletions app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,28 @@
import { LightSmartContractAccount, getDefaultLightAccountFactoryAddress } from "@alchemy/aa-accounts";
import { AlchemyProvider } from "@alchemy/aa-alchemy";
import { LocalAccountSigner, Logger } from "@alchemy/aa-core";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import { TurnkeyClient, createActivityPoller, getWebAuthnAttestation } from "@turnkey/http";
import { createAccount } from "@turnkey/viem";
import { WebauthnStamper } from "@turnkey/webauthn-stamper";
import { deviceName } from "expo-device";
import React, { useCallback } from "react";
import { Platform } from "react-native";
import { Button, Text, XStack, YStack } from "tamagui";
import { Button, Spinner, Text, XStack, YStack } from "tamagui";
import { UAParser } from "ua-parser-js";
import { useBlockNumber, usePublicClient } from "wagmi";
import { useAccount, useConnect, useDisconnect, usePrepareSendTransaction, useSendTransaction } from "wagmi";

import useTurnkeyStore from "../hooks/useTurnkeyStore";
import base64URLEncode from "../utils/base64URLEncode";
import { rpId, turnkeyAPIPublicKey, turnkeyAPIPrivateKey, turnkeyOrganizationId } from "../utils/constants";
import generateRandomBuffer from "../utils/generateRandomBuffer";
import handleError from "../utils/handleError";

if (!process.env.EXPO_PUBLIC_ALCHEMY_API_KEY) throw new Error("missing alchemy api key");
if (!process.env.EXPO_PUBLIC_ALCHEMY_GAS_POLICY_ID) throw new Error("missing alchemy gas policy");
if (!process.env.EXPO_PUBLIC_TURNKEY_ORGANIZATION_ID) throw new Error("missing turnkey organization id");
if (!process.env.EXPO_PUBLIC_TURNKEY_API_PUBLIC_KEY) throw new Error("missing turnkey api public key");
if (!process.env.EXPO_PUBLIC_TURNKEY_API_PRIVATE_KEY) throw new Error("missing turnkey api private key");

const rpId = __DEV__ && Platform.OS === "web" ? "localhost" : "exactly.app";
const apiKey = process.env.EXPO_PUBLIC_ALCHEMY_API_KEY;
const policyId = process.env.EXPO_PUBLIC_ALCHEMY_GAS_POLICY_ID;
const apiPublicKey = process.env.EXPO_PUBLIC_TURNKEY_API_PUBLIC_KEY;
const apiPrivateKey = process.env.EXPO_PUBLIC_TURNKEY_API_PRIVATE_KEY;
const organizationId = process.env.EXPO_PUBLIC_TURNKEY_ORGANIZATION_ID;
Logger.setLogLevel(4);

export default function Home() {
const { chain } = usePublicClient();
const { data: blockNumber } = useBlockNumber();
const { subOrganizationId, signWith, connect: connectTurnkey } = useTurnkeyStore();
const {
connect,
isLoading: isConnecting,
connectors: [connector],
} = useConnect();
const { address } = useAccount();
const { disconnect } = useDisconnect();
const { config: sendConfig } = usePrepareSendTransaction({ to: "0xE72185a9f4Ce3500d6dC7CCDCfC64cf66D823bE8" });
const { sendTransaction, data: txHash, isLoading: isSending } = useSendTransaction(sendConfig);

const create = useCallback(() => {
const createAccount = useCallback(() => {
const name = `exactly, ${new Date().toISOString()}`;
const challenge = generateRandomBuffer();
getWebAuthnAttestation({
Expand All @@ -51,15 +37,15 @@ export default function Home() {
.then(async (attestation) => {
const client = new TurnkeyClient(
{ baseUrl: "https://api.turnkey.com" },
new ApiKeyStamper({ apiPublicKey, apiPrivateKey }),
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,
organizationId: turnkeyOrganizationId,
parameters: {
subOrganizationName: attestation.credentialId,
rootQuorumThreshold: 1,
Expand Down Expand Up @@ -90,77 +76,32 @@ export default function Home() {
},
});
if (!createSubOrganizationResultV4?.wallet?.addresses[0]) throw new Error("sub-org creation failed");
connectTurnkey(
createSubOrganizationResultV4.subOrganizationId,
createSubOrganizationResultV4.wallet.addresses[0],
);
})
.catch(handleError);
}, [connectTurnkey]);
}, []);

const connect = useCallback(() => {
new TurnkeyClient({ baseUrl: "https://api.turnkey.com" }, new WebauthnStamper({ rpId }))
.getWhoami({ organizationId })
.then(async ({ organizationId: subOrgId }) => {
const client = new TurnkeyClient(
{ baseUrl: "https://api.turnkey.com" },
new ApiKeyStamper({ apiPublicKey, apiPrivateKey }),
);
const {
wallets: [wallet],
} = await client.getWallets({ organizationId: subOrgId });
if (!wallet) throw new Error("no wallet");
const { accounts } = await client.getWalletAccounts({ organizationId: subOrgId, walletId: wallet.walletId });
const account = accounts.find(({ curve }) => curve === "CURVE_SECP256K1");
if (!account) throw new Error("no ethereum account");
connectTurnkey(subOrgId, account.address);
})
.catch(handleError);
}, [connectTurnkey]);
const connectAccount = useCallback(() => {
connect({ connector });
}, [connect, connector]);

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

const send = useCallback(() => {
if (!subOrganizationId || !signWith) throw new Error("no wallet");
createAccount({
client: new TurnkeyClient({ baseUrl: "https://api.turnkey.com" }, new WebauthnStamper({ rpId })),
organizationId: subOrganizationId,
ethereumAddress: signWith,
signWith,
})
.then(async (viemAccount) => {
const factoryAddress = getDefaultLightAccountFactoryAddress(chain);
const provider = new AlchemyProvider({ apiKey, chain })
.connect(
(rpcClient) =>
new LightSmartContractAccount({
owner: new LocalAccountSigner(viemAccount),
factoryAddress,
rpcClient,
chain,
}),
)
.withAlchemyGasManager({ policyId }) as AlchemyProvider & { account: LightSmartContractAccount };
const accountAddress = await provider.account.getAddress();
console.log({ signWith, accountAddress, factoryAddress }); // eslint-disable-line no-console
// eslint-disable-next-line no-console
console.log(
await provider.sendTransaction({
to: "0xE72185a9f4Ce3500d6dC7CCDCfC64cf66D823bE8",
from: accountAddress,
value: "0x0",
}),
);
})
.catch(handleError);
}, [chain, subOrganizationId, signWith]);
sendTransaction?.();
}, [sendTransaction]);

return (
<XStack flex={1} alignItems="center" space>
<YStack flex={1} alignItems="center" space>
<Text textAlign="center">block number: {blockNumber && String(blockNumber)}</Text>
<Button onPress={create}>create</Button>
<Button onPress={connect}>connect</Button>
<Button onPress={send} disabled={!subOrganizationId || !signWith}>
send
<Text textAlign="center">{txHash}</Text>
<Button onPress={createAccount}>create</Button>
<Button disabled={isConnecting} onPress={address ? disconnectAccount : connectAccount}>
{isConnecting ? <Spinner size="small" /> : address ?? "connect"}
</Button>
<Button disabled={!address || isSending} onPress={send}>
{isSending ? <Spinner size="small" /> : "send"}
</Button>
</YStack>
</XStack>
Expand Down
11 changes: 4 additions & 7 deletions app/pomelo+api.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { configureChains, createConfig, fetchBlockNumber } from "@wagmi/core";
import { goerli } from "@wagmi/core/chains";
import { alchemyProvider } from "@wagmi/core/providers/alchemy";
import { publicProvider } from "@wagmi/core/providers/public";
import { type ExpoRequest, ExpoResponse } from "expo-router/server";

import { alchemyAPIKey, chain } from "../utils/constants";

const { publicClient, webSocketPublicClient } = configureChains(
[goerli],
[
process.env.EXPO_PUBLIC_ALCHEMY_API_KEY
? alchemyProvider({ apiKey: process.env.EXPO_PUBLIC_ALCHEMY_API_KEY })
: publicProvider(),
],
[chain],
[alchemyAPIKey ? alchemyProvider({ apiKey: alchemyAPIKey }) : publicProvider()],
);
createConfig({ publicClient, webSocketPublicClient });

Expand Down
34 changes: 0 additions & 34 deletions hooks/useTurnkeyStore.ts

This file was deleted.

Loading

0 comments on commit d708888

Please sign in to comment.