The goal of this tutorial is to:
- Implement Privy's template to create a PWA that runs a Next.js application using their embedded wallets.
- Adjust the application to sign and submit transactions for an Account Abstraction Wallet.
Before starting this tutorial, you should have a good understanding of the following:
- React: You need to know how to code with React as this tutorial involves creating a PWA using Next.js, which is based on React.
- Hardhat: You should be familiar with deploying smart contracts using Hardhat.
- Fee Delegation: Understanding fee delegation, especially in the context of VeChain, is crucial for implementing Account Abstraction Wallets.
Privy is a service that provides authentication services, which most users use daily, to access web3 wallets.
Privy handles wallet generation, security aspects, and interfaces that manage the signing of transactions or messages.
Privy offers a setup guide in a template project on GitHub:
https://github.com/privy-io/create-privy-pwa
The steps to be done are:
npx degit privy-io/create-privy-pwa my-pwa-project
Run npm install
to install the project required dependencies:
cd my-pwa-app
npm install
Visit https://privy.io to create a new App.
Create the file .env
and add your created App Id:
NEXT_PUBLIC_PRIVY_APP_ID=
You can start the app with npm run dev
, and you will be able to sign into the app.
For testing development, its recommend to use
ngrok
(guide), since privy makes use of crypto.subtle which requires a secure context (https
).
To adjust the desired login methods, toggle them on or off in the Privy dashboard for the app at: https://dashboard.privy.io
Account Abstraction is a standard that allows smart contracts to initiate transactions, providing an on-chain wallet that can replace an externally owned account (EOA).
Example projects using this standard are available on GitHub at:
- https://github.com/vechain/account-abstraction
- https://github.com/eth-infinitism/account-abstraction
For the purpose of this tutorial, the account abstraction contracts from the SimpleAccount
example by eth-infinitism
are used.
The contracts to be deployed are:
To simplify the process, a modified project is available on GitHub at:
https://github.com/vechain-energy/example-pwa-privy-account-abstraction/tree/main/account-abstraction-contracts
git clone https://github.com/vechain-energy/example-pwa-privy-account-abstraction.git
Run npm install --legacy-peer-deps
to install the project required dependencies:
cd example-pwa-privy-account-abstraction/account-abstraction-contracts/
npm install --legacy-peer-deps
Generate a new private key and create the file .env
, to configure it as deployer:
PRIVATE_KEY=0x...
A simple way to generate a private key can be openssl
:
echo "PRIVATE_KEY=0x$(openssl rand -hex 32)" > .env
Optionally adjust more network related options in the hardhat.config.ts
file.
The deploy scripts are in the deploy/
folder. Inspect for further understanding.
Use the hardhat deploy plugin to deploy the example contracts:
npx hardhat deploy --network vechain_testnet
The output of the command shows the addresses of the deployed contracts:
Generating typings for: 42 artifacts in dir: typechain-types for target: ethers-v6
Successfully generated 120 typings!
Compiled 44 Solidity files successfully (evm target: paris).
deploying "EntryPoint" (tx: 0x3142517bc96bef748c8630af244cf62cb71dd57407470011bf60e7581fd07fba)...: deployed at 0x4C1E3c36d8C0538C88720e0725D7488dc3547fc7 with 4042403 gas
==entrypoint addr= 0x4C1E3c36d8C0538C88720e0725D7488dc3547fc7
deploying "SimpleAccountFactory" (tx: 0xd00c5f3275ae891a1d0259ed5a338d40d6a8689b3ed54abce22180746e5f530a)...: deployed at 0x41Ea0cDa1471d70961bdc81bB9203a09cbf9B65e with 2503344 gas
The deployment addresses and ABIs are archived in the deployments/vechain_testnet
folder.
Please document the address of the deployed SimpleAccountFactory
for future use.
In the above example output, it is 0x30B29698D652f8c82fd3bd10914FDcC108f07865
.
The SimpleAccount
restricts access to its owner or the endpoint contract.
Because Privy does not support the VeChain network, the interaction will be based on signed messages.
A new function executeWithAuthorization
verifies a signature for an execution to be from the owner.
You can add yourself a signature verification to SimpleAccount.sol
with these changes:
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
Call the EIP721 with the constructor by adjusting its definition:
constructor(IEntryPoint anEntryPoint) EIP712("Wallet", "1") {
And add a new function that verifies signatures and does the same as execute
:
/**
* execute a transaction (called directly from owner, or by entryPoint) authorized via signatures
* @param to destination address to call
* @param value the value to pass in this call
* @param data the calldata to pass in this call
* @param validAfter unix timestamp after which the signature will be accepted
* @param validBefore unix timestamp until the signature will be accepted
* @param signature the signed type4 signature
*/
function executeWithAuthorization(
address to,
uint256 value,
bytes calldata data,
uint256 validAfter,
uint256 validBefore,
bytes calldata signature
) external payable {
require(block.timestamp > validAfter, "Authorization not yet valid");
require(block.timestamp < validBefore, "Authorization expired");
/**
* verify that the signature did sign the function call
*/
bytes32 structHash = keccak256(
abi.encode(
keccak256(
"ExecuteWithAuthorization(address to,uint256 value,bytes data,uint256 validAfter,uint256 validBefore)"
),
to,
value,
keccak256(data),
validAfter,
validBefore
)
);
bytes32 digest = _hashTypedDataV4(structHash);
address recoveredAddress = ECDSA.recover(digest, signature);
require(recoveredAddress == owner, "Invalid signer");
// execute the instruction
_call(to, value, data);
}
Extend the .env
file with the following lines:
NEXT_PUBLIC_AA_FACTORY=0x..
NEXT_PUBLIC_NODE_URL=https://testnet.vechain.org
NEXT_PUBLIC_DELEGATOR_URL=https://sponsor-testnet.vechain.energy/by/90
NEXT_PUBLIC_AA_FACTORY
is the address of the previously deployedSimpleAccountFactory
NEXT_PUBLIC_NODE_URL
is the link to the VeChain network to use.NEXT_PUBLIC_DELEGATOR_URL
is a fee delegation service, that will pay the VTHO gas fees. Visit vechain.energy to create one or build one yourself.
Install the VeChain SDK to simplify interaction with the VeChain network:
npm install --save @vechain/sdk-core@1.0.0-beta.16 @vechain/sdk-network@1.0.0-beta.16
A prepared hook will provide connectivity to the Account Abtractions deployment.
Copy useVeChainAccount.tsx
into the components/lib
folder.
The hook internally performs the following steps:
- Generates a random wallet for blockchain interaction, providing an entry point into the network from which transactions can be accepted.
- Requests the user to sign a typed message with the Privy embedded wallet.
- If the SimpleAccount does not exist yet, it injects a clause to deploy a new SimpleAccount first.
- Builds a transaction to interact with the Account Abstraction Wallet.
- Requests the Gas Fee Payment from a Fee Delegator to avoid funding the random wallet with VTHO.
- Sends the transaction to the VeChain Network.
- Returns the transaction ID.
The visualized process is:
sequenceDiagram
participant User
participant App
participant Privy
participant useVeChainAccount
participant SimpleAccountFactory
participant FeeDelegator
App->>+useVeChainAccount: sendTransaction(to, value, data)
useVeChainAccount->>SimpleAccountFactory: getAddress(embeddedWallet.address)
SimpleAccountFactory-->>useVeChainAccount: address
opt address not deployed yet
useVeChainAccount-->>useVeChainAccount: add Factory.createAccount() to transaction
end
useVeChainAccount->>+Privy: ask to sign execute(AA-Address, vetValue, encodedData)
Privy->>User: request signTypedData
User-->>Privy: confirm
Privy-->>-useVeChainAccount: signature
useVeChainAccount->>FeeDelegator: request Fee Delegation
FeeDelegator-->>useVeChainAccount: fee delegation signature
useVeChainAccount->>+SimpleAccount: executeWithAuthorization(.., to, value, data, signature)
SimpleAccount-->Blockchain: execute to.abi(args)
useVeChainAccount-->>-User: confirm tx
Add the VeChainAccountProvider
into the _app.tsx
to wrap the application components.
VeChainAccountProvider
is a React Context provider that does the following:
- Creates a new ThorClient to connect to the VeChain Network.
- Identifies the address of the embedded Privy wallet.
- Loads the
SimpleAccount
address from theSimpleAccountFactory
identified by the user's embedded Privy address. - Exports the address, a function to send transactions, and some generic network/connectivity information.
Import the useVeChainAccount
hook into embedded-wallet.tsx
and call it within the component:
const vechain = useVeChainAccount()
The address exported from the useVeChainAccount
hook is the address from which future transactions will originate.
Add the following snippet to display the account address in the embedded-wallet.tsx
:
<Section>
<p className='text-md mt-2 font-bold uppercase text-gray-300'>
Your VeChain Address
</p>
<textarea
value={vechain.address}
className='mt-4 h-12 w-full rounded-md bg-slate-700 p-4 font-mono text-xs text-slate-50 disabled:opacity-25'
rows={1}
readOnly
/>
</Section>
To test an interaction, replace the existing test transfer with a call to vechain.sendTransaction
.
Replace the onTransfer
function in embedded-wallet.tsx
with the following code:
const onTransfer = async () => {
if (!vechain.address) return
try {
setTxIsLoading(true)
const _txHash = await vechain.sendTransaction({
to: recipientAddress as `0x${string}`,
value: parseEther('0.004'),
})
setTxHash(_txHash)
} catch (e) {
console.error('Transfer failed with error ', e)
setTxIsLoading(false)
}
}
Open two terminals to run the application and create a public https
endpoint:
- Run
npm run dev
to start the PWA application. - Run
ngrok http 3004
to start an ngrok proxy and get a public secure address.
Switch to your browser:
- Open the forwarding address shown by
ngrok
. - Log in with your social account.
- Switch to the
Embedded Wallet
menu.
Visit faucet.vecha.in to claim VET on the TestNet and send 1 VET to the VeChain address shown in your PWA.
In your PWA, enter a valid address for the transfer and test the Transfer function.
A dialog will open and request to sign a transaction. Once signed, the SimpleAccount
will transfer 0.004 VET
to the entered address.
To interact with other smart contracts, sendTransaction
accepts to
, data
, and value
parameters like other similar functions.
Either pass in the data for the smart contract call encoded or pass the data as an object, which will then be used to call encodeFunctionData
within the useVeChainAccount
hook.
For example:
const _txHash = await vechain.sendTransaction({
to: '0x8384738C995D49C5b692560ae688fc8b51af1059',
value: 0,
data: {
abi: [{
"inputs": [],
"name": "increment",
"outputs": [],
"type": "function"
}],
functionName: "increment",
args: []
},
})
Signing messages or transactions can be complex and a hurdle for regular users. To reduce friction, you can disable the signing dialog in the Privy Dashboard under Embedded Wallets > Add confirmation modals
.
Disabling this option allows the application to instantly interact with the blockchain, improving the user experience. Pending transactions can be managed with loading animations.
In this article, we've addressed the lack of VeChain support within Privy and viem by creating an Account Abstraction Factory that serves as the interaction source.
By using Fee Delegation, the gas fees are covered for the user, eliminating the need to ask the user to fund it with VTHO first.
An example application for further exploration of Account Abstractions and a Privy PWA is available on GitHub at vechain-energy/example-pwa-privy-account-abstraction and at pwa-privy.examples.vechain.energy.
You can use the information to either build on top of the example project and hook or derive your own process.