Skip to content

Instantly share code, notes, and snippets.

@kirtirajsinh
Created April 29, 2024 12:25
Show Gist options
  • Select an option

  • Save kirtirajsinh/424cb7640dfb491abd605ca71c68054e to your computer and use it in GitHub Desktop.

Select an option

Save kirtirajsinh/424cb7640dfb491abd605ca71c68054e to your computer and use it in GitHub Desktop.
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
useAccount,
useReadContract,
useSendTransaction,
useSignTypedData,
useSwitchChain,
useWaitForTransactionReceipt,
useWalletClient,
} from "wagmi";
import { getBalance } from "@wagmi/core";
import {
BUNDLER_ADDRESS,
ViemWalletEip712Signer,
bundlerABI,
bytesToHexString,
} from "@farcaster/hub-web";
import { config } from "@/common/helpers/rainbowkit";
import {
WARPCAST_RECOVERY_PROXY,
getDeadline,
getFidForAddress,
getSignedKeyRequestMetadataFromAppAccount,
readNoncesFromKeyGateway,
} from "../helpers/farcaster";
import { formatEther, toBytes, toHex } from "viem";
import {
PENDING_ACCOUNT_NAME_PLACEHOLDER,
useAccountStore,
} from "@/stores/useAccountStore";
import { AccountPlatformType, AccountStatusType } from "../constants/accounts";
import { generateKeyPair } from "../helpers/warpcastLogin";
import { Cog6ToothIcon } from "@heroicons/react/20/solid";
import { glideClient } from "../helpers/glide";
import { NoPaymentOptionsError } from "@paywithglide/glide-js";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { mainnet, base, optimism, polygon, arbitrum } from "wagmi/chains";
const chainOptions = [
{
label: "optimism",
value: optimism.id,
},
{
label: "Ethereum Mainnet",
value: mainnet.id,
},
{
label: "Base",
value: base.id,
},
{
label: "Polygon",
value: polygon.id,
},
{
label: "Arbitrum",
value: arbitrum.id,
},
];
const CreateFarcasterAccount = ({ onSuccess }: { onSuccess?: () => void }) => {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string>();
const [transactionHash, setTransactionHash] = useState<`0x${string}`>("0x");
const [open, setOpen] = useState(false);
const [selectedChainId, setSelectedChainId] = useState(chainOptions[0].value);
const { address, isConnected } = useAccount();
const walletClient = useWalletClient();
const { switchChainAsync } = useSwitchChain();
const { sendTransactionAsync } = useSendTransaction();
const { signTypedDataAsync } = useSignTypedData();
const { accounts, addAccount, setAccountActive } = useAccountStore();
const pendingAccounts = accounts.filter(
(account) =>
account.status === AccountStatusType.pending &&
account.platform === AccountPlatformType.farcaster
);
const { data: price } = useReadContract({
address: BUNDLER_ADDRESS,
abi: bundlerABI,
functionName: "price",
args: [0n],
});
const transactionResult = useWaitForTransactionReceipt({
hash: transactionHash,
});
const getFidAndUpdateAccount = async (): Promise<boolean> => {
console.log(
"getFidAndUpdateAccount",
address,
"pending accounts",
pendingAccounts.length,
"transactionResult",
transactionResult?.data
);
if (!(transactionResult && pendingAccounts.length > 0)) return false;
return getFidForAddress(address!)
.then(async (fid) => {
if (fid) {
const accountId = pendingAccounts[0].id!;
await setAccountActive(accountId, PENDING_ACCOUNT_NAME_PLACEHOLDER, {
platform_account_id: fid.toString(),
data: { signupViaHerocast: true },
});
onSuccess?.();
return true;
}
return false;
})
.catch((e) => {
console.log("error when trying to get fid", e);
setError(`Error when trying to get fid: ${e}`);
return false;
});
};
useEffect(() => {
if (!isConnected || transactionHash === "0x") return;
getFidAndUpdateAccount();
}, [isConnected, transactionHash, transactionResult, pendingAccounts]);
useEffect(() => {
validateWalletHasNoFid();
}, []);
const validateWalletHasNoFid = async (): Promise<boolean> => {
if (!address) return false;
const fid = await getFidForAddress(address);
if (fid) {
setError(
`Wallet ${address} has already registered FID ${fid} - only one account per address`
);
return false;
}
return true;
};
const validateWalletHasGasOnOptimism = async (): Promise<boolean> => {
if (!address) return false;
const { value } = await getBalance(config, {
address,
});
console.log("balance", value, value > 0n);
return value > 0n;
};
const createFarcasterAccount = async () => {
console.log("createFarcasterAccount");
if (!(await validateWalletHasNoFid())) return;
setIsPending(true);
let hexStringPublicKey: `0x${string}`, hexStringPrivateKey: `0x${string}`;
if (!pendingAccounts || pendingAccounts.length === 0) {
const { publicKey, privateKey } = await generateKeyPair();
hexStringPublicKey = bytesToHexString(publicKey)._unsafeUnwrap();
hexStringPrivateKey = bytesToHexString(privateKey)._unsafeUnwrap();
try {
await addAccount({
account: {
status: AccountStatusType.pending,
platform: AccountPlatformType.farcaster,
publicKey: hexStringPublicKey,
privateKey: hexStringPrivateKey,
},
});
} catch (e) {
console.log("error when trying to add account", e);
setIsPending(false);
setError(`Error when trying to add account: ${e}`);
return;
}
} else {
hexStringPublicKey = pendingAccounts[0].publicKey;
hexStringPrivateKey = pendingAccounts[0].privateKey!;
}
const nonce = await readNoncesFromKeyGateway(address!);
const deadline = getDeadline();
const userSigner = new ViemWalletEip712Signer(walletClient.data);
const registerSignatureResponse = await userSigner.signRegister({
to: address,
recovery: WARPCAST_RECOVERY_PROXY,
nonce,
deadline,
});
if (registerSignatureResponse.isErr()) {
console.log(
"error when trying to sign register",
registerSignatureResponse
);
setIsPending(false);
setError(
`Error when trying to sign register: ${JSON.stringify(
registerSignatureResponse
)}`
);
return;
}
const registerSignature = toHex(registerSignatureResponse.value);
const metadata = await getSignedKeyRequestMetadataFromAppAccount(
hexStringPublicKey,
deadline
);
const addSignatureResponse = await userSigner.signAdd({
owner: address,
keyType: 1,
key: toBytes(hexStringPublicKey),
metadataType: 1,
metadata,
nonce,
deadline,
});
if (addSignatureResponse.isErr()) {
console.log("error when trying to sign add", addSignatureResponse);
setError(`Error when trying to sign add: ${addSignatureResponse}`);
setIsPending(false);
return;
}
const addSignature = toHex(addSignatureResponse.value);
try {
if (!address) {
throw new Error("No address");
}
if (selectedChainId === null) {
setError("Please select a chain to proceed.");
setIsPending(false);
return;
}
const registerAccountTransactionHash = await glideClient.writeContract({
account: address,
chainId: selectedChainId,
address: BUNDLER_ADDRESS,
abi: bundlerABI,
functionName: "register",
args: [
{
to: address,
recovery: WARPCAST_RECOVERY_PROXY,
sig: registerSignature,
deadline,
},
[
{
keyType: 1,
key: hexStringPublicKey,
metadataType: 1,
metadata: metadata,
sig: addSignature,
deadline,
},
],
0n,
],
value: price,
switchChainAsync,
sendTransactionAsync,
signTypedDataAsync,
});
console.log(
"registerAccountTransactionHash",
registerAccountTransactionHash
);
setTransactionHash(registerAccountTransactionHash);
} catch (e) {
if (e instanceof NoPaymentOptionsError) {
setError(
"Wallet has no tokens to pay for transaction. Please add tokens to your wallet."
);
setIsPending(false);
return;
}
console.log("error when trying to write contract", e);
const errorStr = String(e).split("Raw Call Arguments")[0];
setError(`when adding account onchain: ${errorStr}`);
setIsPending(false);
return;
}
};
return (
<div className="w-3/4 space-y-4 space-x-4">
<p className="text-[0.8rem] text-muted-foreground">
This will require two wallet signatures and one on-chain transaction.{" "}
<br />
You can pay for the transaction and Farcaster platform fee with ETH or
other tokens on Base, Optimism, Arbitrum, Polygon, or Ethereum.
Farcaster platform fee (yearly) right now is{" "}
{price
? `~${parseFloat(formatEther(price)).toFixed(5)} ETH.`
: "loading..."}
</p>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
>
{selectedChainId !== null
? chainOptions.find((chain) => chain.value === selectedChainId)
?.label
: "Select Chain..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{chainOptions.map((chain) => (
<CommandItem
key={chain.value}
value={chain.value.toString()}
onSelect={(currentValue) => {
setSelectedChainId(parseInt(currentValue));
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedChainId === chain.value
? "opacity-100"
: "opacity-0"
)}
/>
{chain.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<Button
variant="default"
disabled={isPending}
onClick={() => createFarcasterAccount()}
>
Create account
{isPending && (
<div className="pointer-events-none ml-3">
<Cog6ToothIcon
className="h-4 w-4 animate-spin"
aria-hidden="true"
/>
</div>
)}
</Button>
{isPending && (
<Button
variant="outline"
className="ml-4"
onClick={() => getFidAndUpdateAccount()}
>
Manual refresh 🔄
</Button>
)}
{error && (
<div className="flex flex-start items-center mt-2">
<p className="text-wrap break-all text-sm text-red-500">
Error: {error}
</p>
</div>
)}
<div>
<a
href="https://paywithglide.xyz"
target="_blank"
rel="noreferrer"
className="text-sm cursor-pointer text-muted-foreground text-font-medium hover:underline hover:text-blue-500/70"
>
Payments powered by Glide
</a>
</div>
</div>
);
};
export default CreateFarcasterAccount;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment