Skip to content

Commit

Permalink
add agw-react package
Browse files Browse the repository at this point in the history
  • Loading branch information
coffeexcoin committed Sep 16, 2024
1 parent b820633 commit 6222364
Show file tree
Hide file tree
Showing 5 changed files with 4,838 additions and 172 deletions.
67 changes: 67 additions & 0 deletions packages/agw-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "@abstract-foundation/agw-react",
"description": "Abstract Global Wallet React Components",
"version": "0.0.1-alpha.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/abstract-foundation/agw-sdk.git",
"directory": "packages/agw-react"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
},
"type": "module",
"scripts": {
"build": "pnpm run clean && pnpm run build:esm+types",
"build:esm+types": "tsc --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types",
"clean": "rm -rf dist tsconfig.tsbuildinfo",
"typecheck": "tsc --noEmit",
"debug": "tsc-watch --sourceMap true --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types"
},
"main": "./dist/esm/exports/index.js",
"types": "./dist/types/exports/index.d.ts",
"typings": "./dist/types/exports/index.d.ts",
"exports": {
".": {
"types": "./dist/types/exports/index.d.ts",
"default": "./dist/esm/exports/index.js"
}
},
"files": [
"dist",
"src",
"package.json"
],
"dependencies": {
"@privy-io/cross-app-connect": "^0.0.3-beta-20240913012159",
"@privy-io/react-auth": "^1.78.1"
},
"peerDependencies": {
"abitype": "^1.0.0",
"react": "^18.0.0",
"typescript": ">=5.0.4",
"viem": "^2.19.0",
"@abstract-foundation/agw-sdk": "workspace:*"
},
"devDependencies": {
"@abstract-foundation/agw-sdk": "workspace:*",
"@types/react": "^18.3.6",
"viem": "^2.19.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"keywords": [
"eth",
"ethereum",
"smart-account",
"abstract",
"account-abstraction",
"global-wallet",
"wallet",
"web3"
]
}
207 changes: 207 additions & 0 deletions packages/agw-react/src/AbstractWalletProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {
type AbstractClient,
createAbstractClient,
} from "@abstract-foundation/agw-sdk";
import { toPrivyWalletProvider } from "@privy-io/cross-app-connect";
import {
PrivyProvider,
type SignTypedDataParams,
useCrossAppAccounts,
usePrivy,
type User,
} from "@privy-io/react-auth";
import React, { useContext, useEffect, useMemo,useState } from "react";
import {
type Account,
type Address,
type Chain,
custom,
type CustomSource,
type Hex,
hexToString,
toHex,
} from "viem";
import { toAccount } from "viem/accounts";
import { abstractTestnet, ChainEIP712 } from "viem/chains";

/** Interface returned by custom `useSmartAccount` hook */
interface SmartAccountInterface {
/** Privy embedded wallet, used as a signer for the smart account */
signer: Account | undefined;
/** Smart account client to send signature/transaction requests to the smart account */
smartAccountClient: AbstractClient | undefined;
/** Smart account address */
smartAccountAddress: Address | undefined;
/** Boolean to indicate whether the smart account state has initialized */
ready: boolean;
}

const SmartAccountContext = React.createContext<SmartAccountInterface>({
signer: undefined,
smartAccountClient: undefined,
smartAccountAddress: undefined,
ready: false,
});

export const useAbstractGlobalWallet = () => {
return useContext(SmartAccountContext);
};

interface AbstractWalletProviderProps {
appId: string;
defaultChain: Chain;
supportedChains: Chain[];
children: React.ReactNode;
}

const SmartAccountProvider = ({ appId, children }: { appId: string, children: React.ReactNode }) => {
const { signMessage, signTypedData } = useCrossAppAccounts();
const { user, ready, authenticated } = usePrivy();

const account = useMemo(() => {
const getAccountFromCrossAppUser = (user: User) => {
const crossAppAccount = user.linkedAccounts.find(
(account) => account.type === "cross_app"
);
if (
crossAppAccount?.embeddedWallets === undefined ||
crossAppAccount.embeddedWallets.length === 0
) {
throw new Error("No embedded wallet found");
}
const address = crossAppAccount.embeddedWallets[0]?.address;

const signMessageWithPrivy: CustomSource["signMessage"] = async ({
message,
}) => {
let messageString: string;
if (typeof message !== "string") {
if (typeof message.raw === "string") {
messageString = hexToString(message.raw);
} else {
messageString = hexToString(toHex(message.raw));
}
} else {
messageString = message;
}
return signMessage(messageString, {
address,
}) as Promise<`0x${string}`>;
};

const signTransactionWithPrivy: CustomSource["signTransaction"] =
async () => {
throw new Error("Raw transaction signing not currently implemented");
};

// Sanitize the message to ensure it's a valid JSON object
// This is necessary because the message object can contain BigInt values, which
// can't be serialized by JSON.stringify
// TODO: Update this to not modify the underlying message but return a new copy
// with the proper type for the privy side. They are technically the same data but
// the viem typing doesn't play nice with the privy definition.
function sanitizeMessage(message: any) {

Check warning on line 103 in packages/agw-react/src/AbstractWalletProvider.tsx

View workflow job for this annotation

GitHub Actions / publish

Unexpected any. Specify a different type
for (const key in message) {
if (typeof message[key] === "object" && message[key] !== null) {
sanitizeMessage(message[key]);
} else {
if (typeof message[key] === "bigint") {
message[key] = message[key].toString();
}
}
}
}

const signTypedDataWithPrivy: CustomSource["signTypedData"] = async (
data
) => {
sanitizeMessage(data.message);
return signTypedData(data as SignTypedDataParams, {
address,
}) as Promise<`0x${string}`>;
};

return toAccount({
address: address as `0x${string}`,
signMessage: signMessageWithPrivy,
signTransaction: signTransactionWithPrivy,
signTypedData: signTypedDataWithPrivy,
});
};

if (!ready) return;
if (!authenticated) return;
return getAccountFromCrossAppUser(user as User);
}, [ready, authenticated, user, signMessage, signTypedData]);

// States to store the smart account and its status
const [eoa, setEoa] = useState<Account | undefined>();
const [smartAccountClient, setSmartAccountClient] = useState<
AbstractClient | undefined
>();
const [smartAccountAddress, setSmartAccountAddress] = useState<
Hex | undefined
>();
const [smartAccountReady, setSmartAccountReady] = useState(false);

useEffect(() => {
// Creates a smart account given a Privy `ConnectedWallet` object representing
// the user's EOA.
const createSmartWallet = async (eoa: Account) => {
setEoa(eoa);

const privyWalletProvider = toPrivyWalletProvider({
providerAppId: appId,
chains: [abstractTestnet],
});

const smartAccountClient = await createAbstractClient({
signer: eoa,
chain: abstractTestnet as ChainEIP712,
transport: custom(privyWalletProvider)
});

setSmartAccountClient(smartAccountClient);
setSmartAccountAddress(smartAccountClient.account.address);
setSmartAccountReady(true);
};

if (account) createSmartWallet(account);
}, [account]);

return (
<SmartAccountContext.Provider
value={{
ready: smartAccountReady,
smartAccountClient: smartAccountClient,
smartAccountAddress: smartAccountAddress,
signer: eoa,
}}
>
{children}
</SmartAccountContext.Provider>
);
};

export const AbstractWalletProvider = ({
appId,
defaultChain,
supportedChains,
children,
}: AbstractWalletProviderProps) => {
return (
<PrivyProvider
appId={appId}
config={{
embeddedWallets: {
createOnLogin: "off",
noPromptOnSignature: true,
},
defaultChain: defaultChain,
supportedChains: supportedChains,
}}
>
<SmartAccountProvider appId={appId}>{children}</SmartAccountProvider>
</PrivyProvider>
);
};
42 changes: 42 additions & 0 deletions packages/agw-react/src/useLoginWithAbstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useCrossAppAccounts, usePrivy, type User } from '@privy-io/react-auth';
import { useCallback } from 'react';

const AGW_APP_ID = 'cm04asygd041fmry9zmcyn5o5';

interface AbstractGlobalWalletInterface {
/** Boolean to indicate whether the abstract global wallet state has initialized */
ready: boolean;
/** Boolean to indicate whether the user is authenticated */
authenticated: boolean;
/** Privy user object */
user: User | undefined;
/** Function to login with the Abstract global wallet */
login: () => Promise<void>;
/** Function to logout of the abstract global wallet */
logout: () => Promise<void>;
}

export const useLoginWithAbstract = (): AbstractGlobalWalletInterface => {
const { loginWithCrossAppAccount } = useCrossAppAccounts();
const { user, ready, authenticated, logout } = usePrivy();

const login = useCallback(async () => {
if (!ready) return;
if (!authenticated) {
try {
await loginWithCrossAppAccount({ appId: AGW_APP_ID });
} catch (error) {
console.error(error);
return;
}
}
}, [ready, authenticated, loginWithCrossAppAccount]);

return {
ready,
authenticated,
user: user ?? undefined,
login,
logout,
};
};
9 changes: 9 additions & 0 deletions packages/agw-react/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts"],
"compilerOptions": {
"sourceMap": true,
"resolveJsonModule": true
}
}
Loading

0 comments on commit 6222364

Please sign in to comment.