Burner accounts
This example shows how to manage "burner accounts". Starknet React provides
an OverrideAccount
component to override the account used by operations
such as useContractWrite
and useDeployAccount
.
In the following interactive demo, areas with a pink border have a different account context and use the burner account. For an optimal experience, wait for transactions to be confirmed between steps. For a production application, you should wait for transactions to be confirmed by following the pattern described in the send transaction demo.
Account not connected
Step 1: fund burner account
You need to fund the burner account before you can deploy it. We are going to transfer 0.0001 ETH to the burner account.
Burner account context
Step 2: deploy burner account
Deploy the account. The deployment fee will be paid out by the pre-funded amount. The `useDeployAccount` hook deploys the current account, so we call it from inside the burner account context.
Step 3: initialize burner account
The burner account needs to be initialized with a list of allowed calls before it can be used. This step requires a signature from the wallet account, so we step out of the burner context.
Burner account context
Step 4: withdraw funds back to wallet
We can now transfer some of the burner account funds back to the wallet account. Notice how the transaction doesn't require a signature from the wallet account.
Account Context
Starknet React provides an "Account Context" that is used to track the current
account. In most dapps, this context coincides with the currently connected
wallet, but developers can override the current account using the
OverrideAccount
component.
In the next sections, we are going to learn how to use this component to work with burner or arcade accounts.
Helper functions
Before we begin, we need to define some helper functions and constants.
_18// burner account class hash, deployed on Starknet Goerli_18const BURNER_CLASS_HASH =_18 "0x0715b5e10bf63c36e69c402a81e1eb96b9107ef56eb5e821b00893e39bdcf545";_18// burner funding amount_18const FUNDING_AMOUNT = 100_000_000_000_000n;_18_18// track state globally_18const burnerAddressAtom = atom<string | undefined>(undefined);_18const burnerDeployTxAtom = atom<string | undefined>(undefined);_18_18/** Get the Starknet ETH contract. */_18function useNativeCurrency() {_18 const { chain } = useNetwork();_18 return useContract({_18 address: chain.nativeCurrency.address,_18 abi: erc20ABI,_18 });_18}
Root component
We use the root component to initialize the burner account. Since creating the burner account and preparing the deploy transaction are tightly coupled, we do that in a single memo hook.
Notice how the component tree resembles the layout in the demo: components that
send transactions from the burner account are wrapped in a OverrideAccount
component.
_72function RootComponent() {_72 const { provider } = useProvider();_72 const { address } = useAccount();_72_72 const fundedBurnerAddress = useAtomValue(burnerAddressAtom);_72_72 const { deployAccountArgs, burnerAccount } = useMemo(() => {_72 if (!address) {_72 return {_72 deployAccountArgs: undefined,_72 burnerAccount: undefined,_72 };_72 }_72_72 // generate burner account_72 const privateKey = stark.randomAddress();_72 const publicKey = ec.starkCurve.getStarkKey(privateKey);_72_72 const constructorCalldata = CallData.compile({_72 _public_key: publicKey,_72 _master_account: address,_72 });_72_72 const burnerAddress = hash.calculateContractAddressFromHash(_72 publicKey,_72 BURNER_CLASS_HASH,_72 constructorCalldata,_72 0,_72 );_72 const burnerAccount = new Account(provider, burnerAddress, privateKey, "1");_72 // @ts-ignore: Account provider is instantiated to a gateway provider by_72 // default, but we want to keep using the rpc provider._72 burnerAccount.provider = provider;_72 const deployAccountArgs = {_72 classHash: BURNER_CLASS_HASH,_72 constructorCalldata,_72 contractAddress: burnerAddress,_72 addressSalt: publicKey,_72 };_72_72 return {_72 deployAccountArgs,_72 burnerAccount: burnerAccount as AccountInterface,_72 };_72 }, [address, provider]);_72_72 const shortAddress = address_72 ? `${address.slice(0, 8)}...${address.slice(-4)}`_72 : "not connected";_72_72 return (_72 <Card>_72 <CardHeader>_72 <CardTitle>Account {shortAddress}</CardTitle>_72 </CardHeader>_72 <CardContent className="space-y-4">_72 <FundBurnerAccount address={burnerAccount?.address} />_72 <OverrideAccount_72 account={fundedBurnerAddress ? burnerAccount : undefined}_72 >_72 <DeployBurnerAccount deployAccountArgs={deployAccountArgs} />_72 </OverrideAccount>_72 <InitializeBurnerAccount />_72 <OverrideAccount_72 account={fundedBurnerAddress ? burnerAccount : undefined}_72 >_72 <WithdrawFunds walletAddress={address} />_72 </OverrideAccount>_72 </CardContent>_72 </Card>_72 );_72}
Funding the burner account
Before we can deploy the burner account, we need to pre-fund it so that it can
pay for its deploy transaction. We use the useContractWrite
hook to send a
transaction from the wallet account.
_60function FundBurnerAccount({ address }: { address?: string }) {_60 const [fundedAddress, setFundedAddress] = useAtom(burnerAddressAtom);_60_60 const { account: mainAccount } = useAccount();_60 const { contract: eth } = useNativeCurrency();_60_60 const {_60 writeAsync,_60 isLoading,_60 error,_60 } = useContractWrite({_60 calls:_60 eth && mainAccount && address_60 ? [_60 eth.populateTransaction["transfer"]!(_60 address,_60 uint256.bnToUint256(FUNDING_AMOUNT),_60 ),_60 ]_60 : [],_60 });_60 const fundAccount = useCallback(async () => {_60 await writeAsync();_60 setFundedAddress(address);_60 }, [setFundedAddress, address, writeAsync]);_60_60 return (_60 <div className="space-y-4 pb-4">_60 <p>_60 <Checkbox className="mr-2" checked={Boolean(fundedAddress)} />_60 Step 1: fund burner account_60 </p>_60 <p className="text-muted-foreground">_60 You need to fund the burner account before you can deploy it. We are_60 going to transfer 0.0001 ETH to the burner account._60 </p>_60 {fundedAddress ? null : (_60 <Button_60 onClick={() => fundAccount()}_60 className="w-full"_60 disabled={Boolean(!address || fundedAddress || isLoading)}_60 >_60 {isLoading ? (_60 <Loader2 className="h-4 w-4 mr-2 animate-spin" />_60 ) : (_60 <ArrowDownToLine className="h-4 w-4 mr-2" />_60 )}_60 Fund account_60 </Button>_60 )}_60 {error ? (_60 <Alert variant="destructive">_60 <AlertCircle className="h-4 w-4" />_60 <AlertTitle>Error</AlertTitle>_60 <AlertDescription>{error?.message}</AlertDescription>_60 </Alert>_60 ) : null}_60 </div>_60 );_60}
Deploy burner account
The next step is to deploy the burner account. Since the useDeployAccount
hook
deploys the current account, we must place this component inside the context
with the burner account.
_56function DeployBurnerAccount({_56 deployAccountArgs = {},_56}: { deployAccountArgs?: DeployAccountVariables }) {_56 const { address } = useAccount();_56 const [deployTx, setDeployTx] = useAtom(burnerDeployTxAtom);_56 const { deployAccount, data, error, isError, isLoading } =_56 useDeployAccount(deployAccountArgs);_56_56 useEffect(() => {_56 if (data?.transaction_hash) {_56 setDeployTx(data.transaction_hash);_56 }_56 }, [data, setDeployTx]);_56_56 return (_56 <Card className="border-2 border-primary">_56 <CardHeader>_56 <CardTitle>Burner account context</CardTitle>_56 </CardHeader>_56 <CardContent className="space-y-4">_56 <div className="space-y-4">_56 <p>_56 <Checkbox className="mr-2" checked={Boolean(data)} />_56 Step 2: deploy burner account_56 </p>_56 <p className="text-muted-foreground">_56 Deploy the account. The deployment fee will be paid out by the_56 pre-funded amount. The `useDeployAccount` hook deploys the current_56 account, so we call it from inside the burner account context._56 </p>_56 {deployTx ? null : (_56 <Button_56 onClick={() => deployAccount({})}_56 className="w-full"_56 disabled={Boolean(!address || data || isLoading)}_56 >_56 {isLoading ? (_56 <Loader2 className="h-4 w-4 mr-2 animate-spin" />_56 ) : (_56 <Shield className="h-4 w-4 mr-2" />_56 )}_56 Deploy Account_56 </Button>_56 )}_56 {isError ? (_56 <Alert variant="destructive">_56 <AlertCircle className="h-4 w-4" />_56 <AlertTitle>Error</AlertTitle>_56 <AlertDescription>{error?.message}</AlertDescription>_56 </Alert>_56 ) : null}_56 </div>_56 </CardContent>_56 </Card>_56 );_56}
Initialize burner account
This implementation of the burner account requires the user to allow specific actions from the main wallet account. For this demo, we allow calls to ETH's transfer.
_66function InitializeBurnerAccount() {_66 const { contract: eth } = useNativeCurrency();_66 const burnerAddress = useAtomValue(burnerAddressAtom);_66 const { account: walletAccount } = useAccount();_66_66 const {_66 write,_66 data,_66 isLoading,_66 isError,_66 error,_66 } = useContractWrite({_66 calls:_66 burnerAddress && walletAccount && eth_66 ? [_66 {_66 contractAddress: burnerAddress,_66 entrypoint: "update_whitelisted_calls",_66 calldata: [_66 "1",_66 eth.address,_66 selector.getSelectorFromName("transfer"),_66 "1",_66 ],_66 },_66 ]_66 : [],_66 });_66_66 return (_66 <div className="space-y-4 py-4">_66 <p>_66 <Checkbox className="mr-2" checked={Boolean(data)} />_66 Step 3: initialize burner account_66 </p>_66 <p className="text-muted-foreground">_66 The burner account needs to be initialized with a list of allowed calls_66 before it can be used. This step requires a signature from the wallet_66 account, so we step out of the burner context._66 </p>_66 {data ? null : (_66 <Button_66 onClick={() => write()}_66 className="w-full"_66 disabled={_66 Boolean(!walletAccount || !burnerAddress || isLoading)_66 }_66 >_66 {isLoading ? (_66 <Loader2 className="h-4 w-4 mr-2 animate-spin" />_66 ) : (_66 <Lock className="h-4 w-4 mr-2" />_66 )}_66 Initialize account_66 </Button>_66 )}_66 {isError ? (_66 <Alert variant="destructive">_66 <AlertCircle className="h-4 w-4" />_66 <AlertTitle>Error</AlertTitle>_66 <AlertDescription>{error?.message}</AlertDescription>_66 </Alert>_66 ) : null}_66 </div>_66 );_66}
Using the burner account
Now that the burner account is setup, we can use it! We can simply place our
dapp inside the OverrideAccount
context and all hooks like useContractWrite
and useSignTypedData
will use it automatically.
For this demo, we add a button to send some of the funds back to our wallet
account.
Notice how the transaction is submitted without requiring approval from the main wallet.
_59function WithdrawFunds({ walletAddress }: { walletAddress?: string }) {_59 const { contract: eth } = useNativeCurrency();_59 const { address } = useAccount();_59_59 const { write, data, isLoading, isError, error } = useContractWrite({_59 calls:_59 eth && address && walletAddress_59 ? [_59 eth.populateTransaction["transfer"]!(_59 walletAddress,_59 uint256.bnToUint256(FUNDING_AMOUNT / 2n),_59 ),_59 ]_59 : [],_59 });_59_59 return (_59 <Card className="border-2 border-primary">_59 <CardHeader>_59 <CardTitle>Burner account context</CardTitle>_59 </CardHeader>_59 <CardContent className="space-y-4">_59 <div className="space-y-4">_59 <p>_59 <Checkbox className="mr-2" checked={Boolean(data)} />_59 Step 4: withdraw funds back to wallet_59 </p>_59 <p className="text-muted-foreground">_59 We can now transfer some of the burner account funds back to the_59 wallet account. Notice how the transaction doesn't require a_59 signature from the wallet account._59 </p>_59 </div>_59 {data ? null : (_59 <Button_59 onClick={() => write()}_59 className="w-full"_59 disabled={Boolean(!address || !walletAddress || isLoading)}_59 >_59 {isLoading ? (_59 <Loader2 className="h-4 w-4 mr-2 animate-spin" />_59 ) : (_59 <ArrowUpToLine className="h-4 w-4 mr-2" />_59 )}_59 Withdraw funds_59 </Button>_59 )}_59 {data ? <p className="font-mono">{data.transaction_hash}</p> : null}_59 {isError ? (_59 <Alert variant="destructive">_59 <AlertCircle className="h-4 w-4" />_59 <AlertTitle>Error</AlertTitle>_59 <AlertDescription>{error?.message}</AlertDescription>_59 </Alert>_59 ) : null}_59 </CardContent>_59 </Card>_59 );_59}
Conclusion
In this demo, we showed how to use the OverrideAccount
component to change the
account used by hooks inside its context. We used this component to show how to
use a burner account inside our dapp.