Private Transactions
As discussed previously, we need to craft a ZTransaction struct that represents a shielded transaction. This is so that we can invoke the transact method on-chain and execute the private transaction.
function transact(ZTransaction memory ztx) external;We also saw previously how to initialize an instance (variable zkfi ) of SDK.
Creating Transactions
The SDK exposes easy API for developers to specify what transaction they want to send. We will create a transaction using TransactionRequest and TransactionOptions object and pass it to createTransaction(...) method of our SDK instance zkfi.
type TransactionRequest = {
type: TransactionType;
assetIds: number[];
values: bigint[];
feeAssetId: number;
to: string;
payload?: string;
};
export type TransactionOptions = {
revokerId: number;
paymaster?: Hex;
viaBundler?: boolean;
};Since, initially we have no funds in our shielded account, we'd like to deposit some assets into it. Let's say, we'd like to deposit 2 WETH . To achieve this, we create a request with the following parameters:
import { parseEther } from 'viem';
import { TransactionRequest, TransactionOptions, TransactionType } from '@zkfi-tech/shared-types';
const req: TransactionRequest = {
type: TransactionType.DEPOSIT,
assetIds: [65537],
values: [parseEther('2')],
feeAssetId: 0,
to: 'bob.eth',
}
const options: TransactionOptions = {
revokerId: 0
}So what did we do? Let's break it down:
TransactionRequest
We set
typetoTransactionType.DEPOSITsince we want to do a deposit from our public wallet to shielded account.We set
assetIdsto include only one asset of id65537(0x010001in hex) which is the asset id forWETHin Labyrinth. We only want to deposit one asset i.eWETH. You can get a list of all supported assets fromcontractService::getAssets().Let
valuesto include only one value which is2 ether. Since it is at the same index asWETHinassetIds, it is the value ofWETHwe want to deposit.We set
feeAssetIdto0.0is a dummy/placeholder asset id and does not represent any token or asset in reality. We set it to0because in aDEPOSITtransaction, we are not going to pay any fee, from any of the assets mentioned inassetIds. This is because we will be paying gas fees ourselves from our wallets.We set
toto the address, shielded address, or ENS of the target. In this case, it is the recipient of the deposit i.e.bob.eth. We assume that the address associated withbob.ethis already registered with Labyrinth protocol so that it can be ultimately resolved to a shielded address where the funds will be sent.
TransactionOptions: -revokerId: The id of the revoker chosen to keep this transaction compliant.
Now we create the Transaction:
import { Transaction } from '@zkfi-tech/transaction';
import { TransactionRequest, TransactionOptions, TransactionType } from '@zkfi-tech/shared-types';
const req: TransactionRequest = {...};
const options: TransactionOptions = {...};
const tx: Transaction = await zkfi.createTransaction(req, options);Now to authorize this transaction if it needs to be signed. For this, we pass the created transaction instance tx to zkfi.signTransaction(...) the method.
const signedTx: Transaction = await zkfi.signTransaction(tx);We've created the transaction finally! But wait. it is not yet ready to be sent on-chain! Remember we need a shielded transaction ZTransaction for this. To create a shielded transaction we need to hide sensitive values and prove our signed transaction.
Again, zkfiinstance provides a simple proveTransaction(...)method to generate a ZK-SNARK proof proving the validity of the transaction tx and yield an instance of the ZTransaction class.
import { ZTransaction } from '@zkfi-tech/zk-prover';
const ztx: ZTransaction = await zkfi.proveTransaction(tx);Finally, we have a private and fully compliant transaction represented by the ZTransaction instance - ztx . If you want to send this transaction on-chain, you can simply convert it to proper input to the contract's transact by calling ztx.toSolidityInput().
Congrats!
Creating a private asset transfer
Now that we have some funds in our shielded account, let's use it! Let's transfer 1 WETH privately to the alice.eth (again, assuming that the public address associated with alice.eth is already registered).
First, we create a TRANSFER request.
import { parseEther } from 'viem';
import { TransactionRequest, TransactionOptions, TransactionType } from '@zkfi-tech/shared-types';
const req: TransactionRequest = {
type: TransactionType.TRANSFER,
assetIds: [65537],
values: [parseEther('1')],
feeAssetId: 65537,
to: 'alice.eth',
}
const options: TransactionOptions = {
revokerId: 0,
paymaster?: '0xE45c40643af3aa4146E1B1C95051c23f7439ed75',
viaBundler?: true
}We specify the same kind of fields in our request, except this time we specify a feeAssetId of WETH . We specify that we want to pay transaction fees with the WETH asset from our shielded account.
TransactionOptions:
- revokerId: The id of the revoker chosen.
- We set the paymaster to the address of the paymaster contract (ERC-4337), which will sponsor this transaction and will be paid in return for it.
- We finally set theviaBundler option to true, as we want this transaction to be relayed through the bundler thus keeping our privacy intact.
But why and where are we paying fees?
Unlike the DEPOSIT, we don't want to send this transaction with our public wallet. This will expose our wallet's public address on block explorers and privacy will be harmed because anyone can inspect and match the previous deposit transaction and this transfer transaction. Hence, linking them. To resolve this issue, we will send the transaction through a third party, in exchange for some fee. One possible third party is the bundler node from EIP 4337. This fee, paid in WETH, is sent to the bundler ultimately. Note that we don't input the fee value, it is determined using SDK automatically as of now - you can see the value after transaction creation using the tx.feeValue property.
The rest of the steps remain the same - creating, signing and proving the transaction to get the ZTransaction.
const tx: Transaction = await zkfi.createTransaction(req);
const signedTx: Transaction = await zkfi.signTransaction(tx);
const ztx: ZTransaction = await zkfi.proveTransaction(signedTx);Now, you can make calling transact(...) method with ztx.toSolidityInput() an EIP-4337 UserOperation and relay it via bundler.
Withdraw from a shielded account
In case for any reason you want to withdraw assets from your shielded account to any public address you can follow the same steps as the above transfer transaction, but with type set to TransactionType.WITHDRAW.
import { parseEther } from 'viem';
import { TransactionRequest, TransactionType } from '@zkfi-tech/shared-types';
const req: TransactionRequest = {
type: TransactionType.WITHDRAW,
assetIds: [65537],
values: [parseEther('1')],
feeAssetId: 65537,
to: 'carl.eth',
}In this case what you specify as to is a public address or ENS (it is not resolved to a shielded address) where funds will be sent.
const tx: Transaction = await zkfi.createTransaction(req);
const signedTx: Transaction = await zkfi.signTransaction(tx);
const ztx: ZTransaction = await zkfi.proveTransaction(signedTx);Last updated