-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* native privy integration * working privy native login with 1193 provider * refactor & jsdoc type * import fix * remove console.log
- Loading branch information
1 parent
7b102d3
commit ab774ae
Showing
9 changed files
with
2,190 additions
and
73 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
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
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,2 @@ | ||
export { AbstractPrivyProvider } from '../privy/abstractPrivyProvider.js'; | ||
export { useAbstractPrivyLogin } from '../privy/useAbstractPrivyLogin.js'; |
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
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,53 @@ | ||
import { PrivyProvider, type PrivyProviderProps } from '@privy-io/react-auth'; | ||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||
import React from 'react'; | ||
import { type Transport } from 'viem'; | ||
import { abstractTestnet } from 'viem/chains'; | ||
import { createConfig, http, WagmiProvider } from 'wagmi'; | ||
|
||
import { InjectWagmiConnector } from './injectWagmiConnector.js'; | ||
|
||
/** | ||
* Configuration options for the AbstractPrivyProvider. | ||
* @interface AgwPrivyProviderProps | ||
* @extends PrivyProviderProps | ||
* @property {boolean} testnet - Whether to use abstract testnet, defaults to false. | ||
* @property {Transport} transport - Optional transport to use, defaults to standard http. | ||
*/ | ||
interface AgwPrivyProviderProps extends PrivyProviderProps { | ||
testnet?: boolean; | ||
transport?: Transport; | ||
} | ||
|
||
export const AbstractPrivyProvider = ({ | ||
testnet = false, | ||
transport, | ||
...props | ||
}: AgwPrivyProviderProps) => { | ||
const chain = testnet ? abstractTestnet : abstractTestnet; | ||
|
||
const wagmiConfig = createConfig({ | ||
chains: [chain], | ||
ssr: true, | ||
connectors: [], | ||
transports: { | ||
[chain.id]: transport ?? http(), | ||
}, | ||
multiInjectedProviderDiscovery: false, | ||
}); | ||
const queryClient = new QueryClient(); | ||
return ( | ||
<PrivyProvider {...props}> | ||
<WagmiProvider config={wagmiConfig}> | ||
<QueryClientProvider client={queryClient}> | ||
<InjectWagmiConnector | ||
testnet={testnet} | ||
transport={transport ?? http()} | ||
> | ||
{props.children} | ||
</InjectWagmiConnector> | ||
</QueryClientProvider> | ||
</WagmiProvider> | ||
</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,51 @@ | ||
import { Fragment, useEffect, useState } from 'react'; | ||
import React from 'react'; | ||
import type { EIP1193Provider, Transport } from 'viem'; | ||
import { useConfig, useReconnect } from 'wagmi'; | ||
import { injected } from 'wagmi/connectors'; | ||
|
||
import { usePrivyCrossAppProvider } from './usePrivyCrossAppProvider'; | ||
|
||
interface InjectWagmiConnectorProps extends React.PropsWithChildren { | ||
testnet: boolean; | ||
transport: Transport; | ||
} | ||
|
||
export const InjectWagmiConnector = (props: InjectWagmiConnectorProps) => { | ||
const { testnet, transport, children } = props; | ||
|
||
const config = useConfig(); | ||
const { reconnect } = useReconnect(); | ||
const { provider, ready } = usePrivyCrossAppProvider({ testnet, transport }); | ||
const [isSetup, setIsSetup] = useState(false); | ||
|
||
useEffect(() => { | ||
const setup = async (provider: EIP1193Provider) => { | ||
const wagmiConnector = injected({ | ||
target: { | ||
provider, | ||
id: 'xyz.abs.privy', | ||
name: 'Abstract Global Wallet', | ||
icon: '', | ||
}, | ||
}); | ||
|
||
const connector = config._internal.connectors.setup(wagmiConnector); | ||
await config.storage?.setItem('recentConnectorId', 'xyz.abs.privy'); | ||
config._internal.connectors.setState([connector]); | ||
|
||
return connector; | ||
}; | ||
|
||
if (ready && !isSetup) { | ||
setup(provider).then((connector) => { | ||
if (connector) { | ||
reconnect({ connectors: [connector] }); | ||
setIsSetup(true); | ||
} | ||
}); | ||
} | ||
}, [provider, ready]); | ||
|
||
return <Fragment>{children}</Fragment>; | ||
}; |
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,13 @@ | ||
import { useCrossAppAccounts } from '@privy-io/react-auth'; | ||
|
||
import { AGW_APP_ID } from '../constants.js'; | ||
|
||
export const useAbstractPrivyLogin = () => { | ||
const { loginWithCrossAppAccount, linkCrossAppAccount } = | ||
useCrossAppAccounts(); | ||
|
||
return { | ||
login: () => loginWithCrossAppAccount({ appId: AGW_APP_ID }), | ||
link: () => linkCrossAppAccount({ appId: AGW_APP_ID }), | ||
}; | ||
}; |
209 changes: 209 additions & 0 deletions
209
packages/agw-react/src/privy/usePrivyCrossAppProvider.ts
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,209 @@ | ||
import { transformEIP1193Provider } from '@abstract-foundation/agw-client'; | ||
import { | ||
type CrossAppAccount, | ||
type SignTypedDataParams, | ||
useCrossAppAccounts, | ||
usePrivy, | ||
type User, | ||
} from '@privy-io/react-auth'; | ||
import { randomBytes } from 'crypto'; | ||
import { useCallback, useMemo } from 'react'; | ||
import { | ||
type Address, | ||
createPublicClient, | ||
type EIP1193Provider, | ||
type EIP1193RequestFn, | ||
type EIP1474Methods, | ||
fromHex, | ||
http, | ||
type RpcSchema, | ||
type Transport, | ||
} from 'viem'; | ||
import { abstractTestnet } from 'viem/chains'; | ||
|
||
const AGW_APP_ID = 'cm04asygd041fmry9zmcyn5o5'; | ||
|
||
type RpcMethodNames<rpcSchema extends RpcSchema> = | ||
rpcSchema[keyof rpcSchema] extends { Method: string } | ||
? rpcSchema[keyof rpcSchema]['Method'] | ||
: never; | ||
type EIP1474MethodNames = RpcMethodNames<EIP1474Methods>; | ||
|
||
interface UsePrivyCrossAppEIP1193Props { | ||
testnet?: boolean; | ||
transport?: Transport; | ||
} | ||
|
||
export const usePrivyCrossAppProvider = ({ | ||
testnet = false, | ||
transport = http(), | ||
}: UsePrivyCrossAppEIP1193Props) => { | ||
const chain = testnet ? abstractTestnet : abstractTestnet; | ||
|
||
const { | ||
loginWithCrossAppAccount, | ||
linkCrossAppAccount, | ||
// sendTransaction, TBD | ||
signMessage, | ||
signTypedData, | ||
} = useCrossAppAccounts(); | ||
const { user, authenticated, ready } = usePrivy(); | ||
|
||
const passthroughMethods = { | ||
web3_clientVersion: true, | ||
web3_sha3: true, | ||
net_listening: true, | ||
net_peerCount: true, | ||
net_version: true, | ||
eth_blobBaseFee: true, | ||
eth_blockNumber: true, | ||
eth_call: true, | ||
eth_chainId: true, | ||
eth_coinbase: true, | ||
eth_estimateGas: true, | ||
eth_feeHistory: true, | ||
eth_gasPrice: true, | ||
eth_getBalance: true, | ||
eth_getBlockByHash: true, | ||
eth_getBlockByNumber: true, | ||
eth_getBlockTransactionCountByHash: true, | ||
eth_getBlockTransactionCountByNumber: true, | ||
eth_getCode: true, | ||
eth_getFilterChanges: true, | ||
eth_getFilterLogs: true, | ||
eth_getLogs: true, | ||
eth_getProof: true, | ||
eth_getStorageAt: true, | ||
eth_getTransactionByBlockHashAndIndex: true, | ||
eth_getTransactionByBlockNumberAndIndex: true, | ||
eth_getTransactionByHash: true, | ||
eth_getTransactionCount: true, | ||
eth_getTransactionReceipt: true, | ||
eth_getUncleByBlockHashAndIndex: true, | ||
eth_getUncleByBlockNumberAndIndex: true, | ||
eth_getUncleCountByBlockHash: true, | ||
eth_getUncleCountByBlockNumber: true, | ||
eth_maxPriorityFeePerGas: true, | ||
eth_newBlockFilter: true, | ||
eth_newFilter: true, | ||
eth_newPendingTransactionFilter: true, | ||
eth_protocolVersion: true, | ||
eth_sendRawTransaction: true, | ||
eth_uninstallFilter: true, | ||
}; | ||
const passthrough = (method: EIP1474MethodNames) => | ||
!!passthroughMethods[method]; | ||
|
||
const publicClient = createPublicClient({ | ||
chain, | ||
transport, | ||
}); | ||
|
||
const getAddressFromUser = (user: User | null): Address | undefined => { | ||
if (!user) { | ||
return undefined; | ||
} | ||
const crossAppAccount = user.linkedAccounts.find( | ||
(account) => | ||
account.type === 'cross_app' && account.providerApp.id === AGW_APP_ID, | ||
) as CrossAppAccount | undefined; | ||
|
||
const address = crossAppAccount?.embeddedWallets?.[0]?.address; | ||
return address ? (address as Address) : undefined; | ||
}; | ||
|
||
const getAccounts = useCallback( | ||
async (promptLogin: boolean) => { | ||
if (!ready) { | ||
return []; | ||
} | ||
let contextUser = user; | ||
if (promptLogin) { | ||
if (!contextUser && !authenticated) { | ||
contextUser = await loginWithCrossAppAccount({ | ||
appId: AGW_APP_ID, | ||
}); | ||
} else if (!contextUser && authenticated) { | ||
contextUser = await linkCrossAppAccount({ appId: AGW_APP_ID }); | ||
} | ||
} | ||
const address = getAddressFromUser(contextUser); | ||
return address ? [address] : []; | ||
}, | ||
[user, authenticated, ready, loginWithCrossAppAccount, linkCrossAppAccount], | ||
); | ||
|
||
const eventListeners = new Map<string, ((...args: any[]) => void)[]>(); | ||
|
||
const handleRequest = useCallback( | ||
async (request: any) => { | ||
const { method, params } = request; | ||
if (passthrough(method as EIP1474MethodNames)) { | ||
return publicClient.request(request); | ||
} | ||
|
||
switch (method) { | ||
case 'eth_requestAccounts': { | ||
return await getAccounts(true); | ||
} | ||
case 'eth_accounts': { | ||
return await getAccounts(false); | ||
} | ||
case 'wallet_switchEthereumChain': | ||
// TODO: do we need to do anything here? | ||
return null; | ||
case 'wallet_revokePermissions': | ||
// TODO: do we need to do anything here? | ||
return null; | ||
case 'eth_sendTransaction': | ||
case 'eth_signTransaction': | ||
// TODO: Implement | ||
return randomBytes(32).toString('hex'); // fake tx hash | ||
case 'eth_signTypedData_v4': | ||
return await signTypedData( | ||
JSON.parse(params[1]) as SignTypedDataParams, | ||
{ address: params[0] }, | ||
); | ||
case 'eth_sign': | ||
throw new Error('eth_sign is unsafe and not supported'); | ||
case 'personal_sign': { | ||
return await signMessage(fromHex(params[0], 'string'), { | ||
address: params[1], | ||
}); | ||
} | ||
default: | ||
throw new Error(`Unsupported request: ${method}`); | ||
} | ||
}, | ||
[passthrough, publicClient, getAccounts, signMessage], | ||
); | ||
|
||
const provider: EIP1193Provider = useMemo(() => { | ||
return { | ||
on: (event, listener) => { | ||
eventListeners.set(event, [ | ||
...(eventListeners.get(event) ?? []), | ||
listener, | ||
]); | ||
}, | ||
removeListener: (event, listener) => { | ||
eventListeners.set( | ||
event, | ||
(eventListeners.get(event) ?? []).filter((l) => l !== listener), | ||
); | ||
}, | ||
request: handleRequest as EIP1193RequestFn<EIP1474Methods>, | ||
}; | ||
}, [handleRequest]); | ||
|
||
const wrappedProvider = transformEIP1193Provider({ | ||
chain, | ||
provider, | ||
transport, | ||
}); | ||
|
||
return { | ||
ready, | ||
provider: wrappedProvider, | ||
}; | ||
}; |
Oops, something went wrong.