-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b820633
commit 6222364
Showing
5 changed files
with
4,838 additions
and
172 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.