Skip to content

Commit

Permalink
feat: Add polygon mainnet support and fix wallet signature issue (#13)
Browse files Browse the repository at this point in the history
* feat: integrate sdk + nft contract for onboarding

* feat: fix signing, add local storage wrapper, and fix up smaller ui issues

* feat: add mainnet polygon as supported chain
  • Loading branch information
rthomare authored Jun 7, 2023
1 parent 7dd7c97 commit a67970a
Show file tree
Hide file tree
Showing 16 changed files with 277 additions and 111 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ The primary interfaces are the `SmartAccountProvider` and `BaseSmartContractAcco
The `SmartAccountProvider` is an ERC-1193 compliant Provider that wraps JSON RPC methods and some Wallet Methods (signing, sendTransaction, etc). It also provides two utility methods for sending UserOperations:

1. `sendUserOperation` -- this takes in `target`, `callData`, and an optional `value` which then constructs a UserOperation (UO), sends it, and returns the `hash` of the UO. It handles estimating gas, fetching fee data, (optionally) requesting paymasterAndData, and lastly signing. This is done via a middleware stack that runs in a specific order. The middleware order is `getDummyPaymasterData` => `estimateGas` => `getFeeData` => `getPaymasterAndData`. The paymaster fields are set to `0x` by default. They can be changed using `provider.withPaymasterMiddleware`.
2. `sendTransaction` -- this takes in a traditional Transaction Request object which then gets converted into a UO. Currently, the only data being used from the Transaction Request object is `to`, `callData` and `value`. Support for other fields is coming soon.
2. `sendTransaction` -- this takes in a traditional Transaction Request object which then gets converted into a UO. Currently, the only data being used from the Transaction Request object is `from`, `to`, `data` and `value`. Support for other fields is coming soon.

If you want to add support for your own `SmartAccounts` then you will need to provide an implementation of `BaseSmartContractAccount`. You can see an example of this in [SimpleSmartContractAccount](packages/core/src/account/simple.ts). You will need to implement 4 methods:

Expand Down
31 changes: 17 additions & 14 deletions examples/alchemy-daapp/src/clients/appState.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,64 @@
import { useAccount } from "wagmi";
import { useNFTsQuery } from "../surfaces/profile/NftSection";
import { useAccount, useChainId } from "wagmi";
import { useEffect, useState } from "react";
import { localSmartContractStore } from "./localStorage";

export type AppState =
| {
state: "LOADING";
eoaAddress: undefined;
scwAddress: undefined;
scwAddresses: undefined;
}
| {
state: "UNCONNECTED";
eoaAddress: undefined;
scwAddress: undefined;
scwAddresses: undefined;
}
| {
state: "NO_SCW";
eoaAddress: string;
scwAddress: undefined;
scwAddresses: undefined;
}
| {
state: "HAS_SCW";
eoaAddress: string;
scwAddress: string;
scwAddresses: string[];
};

export function useAppState(): AppState {
const { address, isConnected } = useAccount();
const scwAddress = address ? localStorage.getItem(address) : undefined;
const chainId = useChainId();

const [state, setState] = useState<AppState>({
state: "UNCONNECTED",
eoaAddress: undefined,
scwAddress: undefined,
scwAddresses: undefined,
});
useEffect(() => {
if (!isConnected || !address) {
setState({
state: "UNCONNECTED",
eoaAddress: undefined,
scwAddress: undefined,
scwAddresses: undefined,
});
return;
}

if (!scwAddress) {
const scwAddresses = localSmartContractStore.smartAccountAddresses(
address,
chainId
);
if (scwAddresses.length === 0) {
setState({
state: "NO_SCW",
eoaAddress: address as `0x${string}`,
scwAddress: undefined,
scwAddresses: undefined,
});
} else {
setState({
state: "HAS_SCW",
eoaAddress: address as `0x${string}`,
scwAddress: scwAddress!,
scwAddresses: scwAddresses,
});
}
}, [address, isConnected, scwAddress]);
}, [address, isConnected, chainId]);
return state;
}
38 changes: 38 additions & 0 deletions examples/alchemy-daapp/src/clients/localStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const key = (address: string, chainId: number) =>
`smartContractAccount-${address}-${chainId}`;

const smartAccountAddresses = (address: string, chainId: number) => {
const scAddresses = JSON.parse(
localStorage.getItem(key(address, chainId)) ?? "[]"
);
return scAddresses;
};

const addSmartContractAccount = (
address: string,
scAddress: string,
chainId: number
) => {
const k = key(address, chainId);
const addresses = JSON.parse(localStorage.getItem(k) ?? "[]");
localStorage.setItem(k, JSON.stringify([...addresses, scAddress]));
};

const removeSmartContractAccount = (
address: string,
scAddress: string,
chainId: number
) => {
const k = key(address, chainId);
const scAddresses = JSON.parse(localStorage.getItem(k) ?? "[]");
if (!scAddresses.includes(scAddress)) {
scAddresses.splice(scAddresses.indexOf(scAddress), 1);
localStorage.setItem(k, JSON.stringify([...scAddresses, scAddress]));
}
};

export const localSmartContractStore = {
smartAccountAddresses,
addSmartContractAccount,
removeSmartContractAccount,
};
41 changes: 41 additions & 0 deletions examples/alchemy-daapp/src/clients/simpleAccountOwner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { SimpleSmartAccountOwner } from "@alchemy/aa-core";
import { useCallback } from "react";
import { toHex } from "viem";
import { useWalletClient } from "wagmi";

type SimpleSmartAccountOwnerResult =
| {
isLoading: false;
owner: SimpleSmartAccountOwner;
}
| {
isLoading: true;
owner: undefined;
};

export function useSimpleAccountOwner(): SimpleSmartAccountOwnerResult {
const walletClientQuery = useWalletClient();
// We need this to by pass a viem bug https://github.com/wagmi-dev/viem/issues/606
const signMessage = useCallback(
(data: string | Uint8Array) =>
walletClientQuery.data!.request({
// ignore the type error here, it's a bug in the viem typing
// @ts-ignore
method: "personal_sign",
// @ts-ignore
params: [toHex(data), walletClientQuery.data.account.address],
}),
[walletClientQuery.data]
);
const getAddress = useCallback(
() => Promise.resolve(walletClientQuery.data!.account.address),
[walletClientQuery.data]
);
if (walletClientQuery.isLoading) {
return {
isLoading: true,
owner: undefined,
};
}
return { isLoading: false, owner: { signMessage, getAddress } };
}
9 changes: 8 additions & 1 deletion examples/alchemy-daapp/src/configs/clientConfigs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Chain, polygonMumbai, sepolia } from "viem/chains";
import { Chain, polygon, polygonMumbai, sepolia } from "viem/chains";

export interface DAAppConfiguration {
nftContractAddress: `0x${string}`;
Expand All @@ -10,6 +10,13 @@ export interface DAAppConfiguration {

// TODO: Replace with your own contract addresses and policy ids, feel free to add or remove chains.
export const daappConfigurations: Record<number, DAAppConfiguration> = {
[polygon.id]: {
nftContractAddress: "0xb7b9424ef3d1b9086b7e53276c4aad68a1dd971c",
simpleAccountFactoryAddress: "0x15Ba39375ee2Ab563E8873C8390be6f2E2F50232",
gasManagerPolicyId: "f0f6fb99-28b0-4e9e-a40d-201ceb1f2b3b",
rpcUrl: `/api/rpc/proxy?chainId=${polygon.id}`,
chain: polygon,
},
[polygonMumbai.id]: {
nftContractAddress: "0x5679b0de84bba361d31b2e7152ab20f0f8565245",
simpleAccountFactoryAddress: "0x9406Cc6185a346906296840746125a0E44976454",
Expand Down
3 changes: 2 additions & 1 deletion examples/alchemy-daapp/src/configs/serverConfigs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { polygonMumbai, sepolia } from "viem/chains";
import { polygon, polygonMumbai, sepolia } from "viem/chains";
import { env } from "~/env.mjs";

// TODO: Replace with your own api urls per chain.
const API_URLs: Record<number, string> = {
[polygonMumbai.id]: env.MUMBAI_ALCHEMY_API_URL,
[sepolia.id]: env.SEPOLIA_ALCHEMY_API_URL,
[polygon.id]: env.POLYGON_ALCHEMY_API_URL,
};

export function getApiUrl(chainId: number | string) {
Expand Down
2 changes: 2 additions & 0 deletions examples/alchemy-daapp/src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const env = createEnv({
NODE_ENV: z.enum(["development", "test", "production"]),
MUMBAI_ALCHEMY_API_URL: z.string().url(),
SEPOLIA_ALCHEMY_API_URL: z.string().url(),
POLYGON_ALCHEMY_API_URL: z.string().url(),
},

/**
Expand All @@ -25,6 +26,7 @@ export const env = createEnv({
*/
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
POLYGON_ALCHEMY_API_URL: process.env.POLYGON_ALCHEMY_API_URL,
MUMBAI_ALCHEMY_API_URL: process.env.MUMBAI_ALCHEMY_API_URL,
SEPOLIA_ALCHEMY_API_URL: process.env.SEPOLIA_ALCHEMY_API_URL,
},
Expand Down
6 changes: 5 additions & 1 deletion examples/alchemy-daapp/src/http/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { OwnedNFTsResponse } from "../declarations/api";
import { callEndpoint } from "./http";

export function getNFTs(address: string): Promise<OwnedNFTsResponse> {
export function getNFTs(
address: string,
chainId: number
): Promise<OwnedNFTsResponse> {
return callEndpoint("GET", "/api/nfts", {
address,
chainId,
});
}
3 changes: 2 additions & 1 deletion examples/alchemy-daapp/src/pages/api/rpc/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
return res.send(
await callEndpoint("POST", getApiUrl(chainId as string), req.body)
);
} catch {
} catch (e) {
console.error(e);
return res.status(500).send("Internal Server Error");
}
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { signMessage } from "@wagmi/core";
import { toHex, encodeFunctionData } from "viem";
import { encodeFunctionData } from "viem";
import {
createPublicErc4337Client,
SimpleSmartContractAccount,
Expand All @@ -22,6 +21,7 @@ import {
initialStep,
metaForStepIdentifier,
} from "./OnboardingDataModels";
import { localSmartContractStore } from "~/clients/localStorage";

async function pollForLambdaForComplete(
lambda: () => Promise<boolean>,
Expand All @@ -37,7 +37,7 @@ async function pollForLambdaForComplete(
}
} while (!reciept && txnRetryCount++ < txnMaxDurationSeconds);
if (!reciept) {
throw new Error("Timedout waiting for contrat deployment and NFT mint.");
throw new Error("Timedout waiting for processs completion.");
}
return reciept;
}
Expand All @@ -54,23 +54,17 @@ const onboardingStepHandlers: Record<
OnboardingStepIdentifier,
OnboardingFunction
> = {
// This is the first step it checks for and creates the owener signer.
// This is the first step it checks for the owner signer.
[OnboardingStepIdentifier.INITIAL_STEP]: async (context) => {
if (!context.ownerAddress) {
throw new Error("No connected account or address");
}
const owner: SimpleSmartAccountOwner = {
signMessage: async (msg) =>
signMessage({
message: toHex(msg),
}),
getAddress: async () => context.ownerAddress!,
};
if (!context.owner) {
throw new Error("No connected account or address");
}
return {
nextStep: OnboardingStepIdentifier.GET_ENTRYPOINT,
addedContext: {
owner,
},
addedContext: {},
};
},
// This step gets the entrypoint for the smart account.
Expand Down Expand Up @@ -174,19 +168,20 @@ const onboardingStepHandlers: Record<
if (!context.smartAccountSigner?.account || !targetAddress) {
throw new Error("No SCW account was found");
}
const { hash: mintDeployOpHash } =
await context.smartAccountSigner.sendUserOperation(
appConfig.nftContractAddress,
encodeFunctionData({
abi: NFTContractABI.abi,
functionName: "mintTo",
args: [targetAddress],
})
);
const mintDeployTxnHash = await context.smartAccountSigner.sendTransaction({
from: await context.smartAccountSigner.account.getAddress(),
to: appConfig.nftContractAddress,
data: encodeFunctionData({
abi: NFTContractABI.abi,
functionName: "mintTo",
args: [targetAddress],
}),
});

return {
nextStep: OnboardingStepIdentifier.CHECK_OP_COMPLETE,
addedContext: {
mintDeployOpHash: mintDeployOpHash as `0x${string}`,
mintDeployTxnHash: mintDeployTxnHash as `0x${string}`,
},
};
},
Expand All @@ -196,11 +191,11 @@ const onboardingStepHandlers: Record<
*/
[OnboardingStepIdentifier.CHECK_OP_COMPLETE]: async (context) => {
await pollForLambdaForComplete(async () => {
if (!context.mintDeployOpHash) {
if (!context.mintDeployTxnHash) {
throw new Error("No mint deploy operation Hash was found");
}
return context
.client!.getUserOperationReceipt(context.mintDeployOpHash)
.client!.getTransactionReceipt({ hash: context.mintDeployTxnHash })
.then((receipt) => {
return receipt !== null;
});
Expand All @@ -222,7 +217,11 @@ const onboardingStepHandlers: Record<
if (!context.smartAccountAddress) {
throw new Error("No SCW was found");
}
localStorage.setItem(inMemOwnerAddress, context.smartAccountAddress);
localSmartContractStore.addSmartContractAccount(
inMemOwnerAddress,
context.smartAccountAddress,
context.chain?.id!
);
return {
nextStep: OnboardingStepIdentifier.DONE,
addedContext: {},
Expand All @@ -237,7 +236,10 @@ const onboardingStepHandlers: Record<
},
};

export function useOnboardingOrchestrator(useGasManager: boolean) {
export function useOnboardingOrchestrator(
useGasManager: boolean,
owner: SimpleSmartAccountOwner
) {
// Setup initial data and state
const { address: ownerAddress } = useAccount();
const { chain } = useNetwork();
Expand All @@ -258,12 +260,15 @@ export function useOnboardingOrchestrator(useGasManager: boolean) {
return { client, appConfig };
}, [chain]);
const [currentStep, updateStep] = useState<OnboardingStep>(
initialStep(ownerAddress!, client, chain!, useGasManager)
initialStep(owner!, ownerAddress!, client, chain!, useGasManager)
);
const [isLoading, setIsLoading] = useState(false);

const reset = useCallback(
() => updateStep(initialStep(ownerAddress!, client, chain!, useGasManager)),
() =>
updateStep(
initialStep(owner!, ownerAddress!, client, chain!, useGasManager)
),
[ownerAddress, client, chain, useGasManager]
);

Expand Down Expand Up @@ -318,6 +323,7 @@ export function useOnboardingOrchestrator(useGasManager: boolean) {
});
}
} catch (e) {
console.error(e);
throw e;
} finally {
setIsLoading(false);
Expand Down
Loading

0 comments on commit a67970a

Please sign in to comment.