diff --git a/frontend/app/backups/page.jsx b/frontend/app/backups/page.jsx index 22630de..ecc8b30 100644 --- a/frontend/app/backups/page.jsx +++ b/frontend/app/backups/page.jsx @@ -35,7 +35,7 @@ import useUniversalAccountInfo from '@/hooks/useUniversalAccountInfo'; // const moduleAddress = '0xbDa1dE70eAE1A18BbfdCaE95B42b5Ff6d3352492'; // const ownerAddress = '0xED9586AD3a6A512ce5c2d0C6a5bf8972c00137e2'; -const ownerAddress = '0x90382784cFa7bE80Eb4107C0640e6D9195823B3B'; +// const ownerAddress = '0x90382784cFa7bE80Eb4107C0640e6D9195823B3B'; // const getBackupsAbi = [ // { @@ -85,20 +85,24 @@ const ownerAddress = '0x90382784cFa7bE80Eb4107C0640e6D9195823B3B'; // console.error('Error:', error); // }); +import { useRouter } from 'next/navigation' + console.log('render'); export default function BackUpsPage() { const { isModuleSupported, isWingmanDeployed, smartAccountClient } = useSmartAccountClient(); - const { address } = useUniversalAccountInfo(); + // const { address } = useUniversalAccountInfo(); const [backupsList, setBackupsList] = useState([]); const [detailedBackupsList, setDetailedBackupsList] = useState([]); const [combinedBackups, setCombinedBackups] = useState([]); + const { setSafeInfo, safeInfo } = useSafeInfoContextProvider(); + useEffect(() => { - console.log('ADDRESS', address); - if (!address) { - getBackups(ownerAddress) + console.log('ADDRESS', safeInfo.address); + if (!safeInfo.address) { + getBackups(safeInfo.address) .then((backups) => { setBackupsList(backups); console.log('Backups:', backups); @@ -115,7 +119,7 @@ export default function BackUpsPage() { try { const detailedBackups = await Promise.all( backupsList.map((backupName) => - getBackup(ownerAddress, backupName), + getBackup(safeInfo.address, backupName), ), ); @@ -168,7 +172,7 @@ export default function BackUpsPage() {
{Array.isArray(user.beneficiaries) ? user.beneficiaries.map((item) => ( -

{item.percentage}

+

{item.percentage}%

)) : user.beneficiaries}
@@ -201,16 +205,17 @@ export default function BackUpsPage() { } }, []); + const router = useRouter(); + return (

Your Backups

diff --git a/frontend/app/create/page.js b/frontend/app/create/page.js new file mode 100644 index 0000000..d5e03e1 --- /dev/null +++ b/frontend/app/create/page.js @@ -0,0 +1,141 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@nextui-org/button'; +import { Input } from '@nextui-org/input'; +import { DatePicker } from '@nextui-org/date-picker'; +import { Switch } from '@nextui-org/switch'; +import { DateValue, parseDate } from '@internationalized/date'; + +import CustomTooltip from '@/components/CustomTooltip'; +import {walletClientToSmartAccountSigner} from "permissionless"; +import {useWalletClient} from "wagmi"; + +import { useSafeInfoContextProvider } from "@/context/SafeInfoContextProvider"; +import {createBackup} from "../../services/createBackup"; + +export default function CreatePage() { + const [valueName, setValueName] = useState(''); + const [isFixed, setIsFixed] = useState(true); + const [valueReceiver, setValueReceiver] = useState(''); + const [valueNumber, setValueNumber] = useState(''); + const [valueDate, setValueDate] = useState( + parseDate('2024-07-14'), + ); + + const { data: walletClient } = useWalletClient(); + + const { setSafeInfo, safeInfo } = useSafeInfoContextProvider(); + + async function handleCreateBackup() { + const smartAccountSigner = + await walletClientToSmartAccountSigner(walletClient); + + console.log(smartAccountSigner); + + const date = valueDate.toDate(); + const seconds = date.getTime() / 1000; + + console.log('safe info', safeInfo) + + console.log({ + smartAccountSigner, + valueName, + seconds, + valueReceiver, + number: +valueNumber + }) + + createBackup(smartAccountSigner, safeInfo.address, valueName, seconds, valueReceiver, +valueNumber) + .then(() => console.log('success')) + } + + return ( +
+

Create Backup

+ {/* ================= */} +
+ + +
+ +
+
+
+ +
+
+ Value +
+ % + + Fixed +
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+ + +
+ {/* ================= */} +
+ +
+
+ ); +} diff --git a/frontend/app/page.js b/frontend/app/page.js index 596584b..1c4d52c 100644 --- a/frontend/app/page.js +++ b/frontend/app/page.js @@ -21,6 +21,8 @@ import { prepareSmartAccountClient, } from "@/services/prepareSmartAccountClient"; +import { useRouter } from 'next/navigation' + export default function Home() { const { connectedTo, address } = useUniversalAccountInfo(); @@ -225,6 +227,7 @@ function StageThree() { } function StageFour() { + const router = useRouter() return ( <> {/* eslint-disable-next-line react/no-unescaped-entities */} @@ -232,10 +235,9 @@ function StageFour() { diff --git a/frontend/components/AccountRadioGroup.js b/frontend/components/AccountRadioGroup.js index c183f6d..4196329 100644 --- a/frontend/components/AccountRadioGroup.js +++ b/frontend/components/AccountRadioGroup.js @@ -13,7 +13,7 @@ export default function AccountRadioGroup({ safes, onChange }) { className=" py-2 bg-primary-200" isDisabled={!isCompatible} size="md w-full" - onClick={onChange} + onClick={() => onChange(safe.address)} > {safe.address} diff --git a/frontend/context/SafeInfoContextProvider.js b/frontend/context/SafeInfoContextProvider.js index 8a5f62c..c0c7dae 100644 --- a/frontend/context/SafeInfoContextProvider.js +++ b/frontend/context/SafeInfoContextProvider.js @@ -1,3 +1,5 @@ +'use client' + import React, { useState, useContext, useEffect } from "react"; import { useWalletClient } from "wagmi"; import { walletClientToSmartAccountSigner } from "permissionless"; @@ -28,6 +30,10 @@ export function SafeInfoContextProvider({ children }) { const { data: walletClient } = useWalletClient(); + useEffect(() => { + console.log('safe info', safeInfo); + }, [safeInfo]) + useEffect(() => { if (!walletClient) return; (async () => { diff --git a/frontend/package.json b/frontend/package.json index 4909d10..f7bcaf6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix" }, "dependencies": { + "@internationalized/date": "^3.5.4", "@nextui-org/button": "2.0.34", "@nextui-org/code": "2.0.29", "@nextui-org/date-picker": "^2.1.2", diff --git a/frontend/services/createBackup.js b/frontend/services/createBackup.js new file mode 100644 index 0000000..2a4cba3 --- /dev/null +++ b/frontend/services/createBackup.js @@ -0,0 +1,164 @@ +import { + ENTRYPOINT_ADDRESS_V07, + bundlerActions, + getSenderAddress, + signUserOperationHashWithECDSA, +} from "permissionless" +import { pimlicoBundlerActions, pimlicoPaymasterActions } from "permissionless/actions/pimlico" +import { createClient, createPublicClient, encodeFunctionData, http } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import { sepolia } from "viem/chains" + +const generateBackupAbiWithArgs = ({name, unlockAt, beneficiaries}) => ({ + abi: [ + { + name: "updateBackup", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'uint48', name: 'unlockAt', type: 'uint48' }, + { + internalType: 'struct Wingman.Beneficiary[]', + name: 'beneficiaries', + type: 'tuple[]', + components: [ + { internalType: 'address', name: 'account', type: 'address' }, + { internalType: 'uint8', name: 'percentage', type: 'uint8' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' } + ] + } + ], + outputs: [] + } + ], + functionName: "updateBackup", + args: [name, unlockAt, beneficiaries] +}) + +const publicClient = createPublicClient({ + transport: http("https://rpc.ankr.com/eth_sepolia"), + chain: sepolia, +}) + +const apiKey = "5499b16e-f9de-427c-8cd1-76417e0a7d22" +const endpointUrl = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${apiKey}` + +const bundlerClient = createClient({ + transport: http(endpointUrl), + chain: sepolia, +}) + .extend(bundlerActions(ENTRYPOINT_ADDRESS_V07)) + .extend(pimlicoBundlerActions(ENTRYPOINT_ADDRESS_V07)) + +const paymasterClient = createClient({ + transport: http(endpointUrl), + chain: sepolia, +}).extend(pimlicoPaymasterActions(ENTRYPOINT_ADDRESS_V07)) + +// const ownerPrivateKey = "0x30320097c1d7009d6d970376c792fe157a5e989f057b8908345043393a56a8a5" +// const owner = privateKeyToAccount(ownerPrivateKey) + +export async function createBackup(owner, smartAccountAddress, name, unlockAt, benefAddress) { + const factory = "0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985" + const factoryData = encodeFunctionData({ + abi: [ + { + inputs: [ + { name: "owner", type: "address" }, + { name: "salt", type: "uint256" }, + ], + name: "createAccount", + outputs: [{ name: "ret", type: "address" }], + stateMutability: "nonpayable", + type: "function", + }, + ], + args: [owner.address, 0n], + }) + + console.log("Generated factoryData:", factoryData) + + const senderAddress = await getSenderAddress(publicClient, { + factory, + factoryData, + entryPoint: ENTRYPOINT_ADDRESS_V07, + }) + console.log("Calculated sender address:", senderAddress) + + const updateBackupCallData = encodeFunctionData( + generateBackupAbiWithArgs({name: name, unlockAt: unlockAt, beneficiaries: [ + { + account: benefAddress, + percentage: 100, + amount: 0 + } + ]}) + ); + + const callData = encodeFunctionData({ + abi: [ + { + inputs: [ + { name: "dest", type: "address" }, + { name: "value", type: "uint256" }, + { name: "func", type: "bytes" }, + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + ], + args: ["0xbDa1dE70eAE1A18BbfdCaE95B42b5Ff6d3352492", 0, updateBackupCallData], + }) + + console.log("Generated callData:", callData) + + const gasPrice = await bundlerClient.getUserOperationGasPrice() + + const userOperation = { + sender: smartAccountAddress, + nonce: 0n, + factory: factory, + factoryData, + callData, + maxFeePerGas: gasPrice.fast.maxFeePerGas, + maxPriorityFeePerGas: gasPrice.fast.maxPriorityFeePerGas, + // dummy signature, needs to be there so the SimpleAccount doesn't immediately revert because of invalid signature length + signature: + "0xa15569dd8f8324dbeabf8073fdec36d4b754f53ce5901e283c6de79af177dc94557fa3c9922cd7af2a96ca94402d35c39f266925ee6407aeb32b31d76978d4ba1c", + } + + const sponsorUserOperationResult = await paymasterClient.sponsorUserOperation({ + userOperation, + }) + + const sponsoredUserOperation = { + ...userOperation, + ...sponsorUserOperationResult, + } + + console.log("Received paymaster sponsor result:", sponsorUserOperationResult) + + const signature = await signUserOperationHashWithECDSA({ + account: owner, + userOperation: sponsoredUserOperation, + chainId: sepolia.id, + entryPoint: ENTRYPOINT_ADDRESS_V07, + }) + sponsoredUserOperation.signature = signature + + + const userOperationHash = await bundlerClient.sendUserOperation({ + userOperation: sponsoredUserOperation, + }) + + + const receipt = await bundlerClient.waitForUserOperationReceipt({ + hash: userOperationHash, + }) + const txHash = receipt.receipt.transactionHash + + console.log(`UserOperation included: https://sepolia.etherscan.io/tx/${txHash}`) +}