Demos
Burner accounts

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
_18
const BURNER_CLASS_HASH =
_18
"0x0715b5e10bf63c36e69c402a81e1eb96b9107ef56eb5e821b00893e39bdcf545";
_18
// burner funding amount
_18
const FUNDING_AMOUNT = 100_000_000_000_000n;
_18
_18
// track state globally
_18
const burnerAddressAtom = atom<string | undefined>(undefined);
_18
const burnerDeployTxAtom = atom<string | undefined>(undefined);
_18
_18
/** Get the Starknet ETH contract. */
_18
function 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.


_72
function 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.


_60
function 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.


_56
function 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.


_66
function 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.


_59
function 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.