zkLogin for DeFi
DeFi applications on Sui can use zkLogin to onboard users who do not have a Sui wallet. Combined with Enoki's sponsored transactions, this creates a trading experience where the user signs in with Google, swaps tokens, and the application covers gas fees, eliminating the cold-start problem of needing SUI before you can trade.
The flow uses Enoki end-to-end: createSponsoredTransaction builds and sponsors the transaction server-side, the wallet signs the returned bytes client-side, and executeSponsoredTransaction submits the signed result.
Step 1: Set up dApp Kit with Enoki wallets
Register Enoki wallets with dApp Kit so users can connect through the standard wallet UI.
import { createDAppKit } from '@mysten/dapp-kit-react';
import { registerEnokiWallets } from '@mysten/enoki';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const GRPC_URLS = {
mainnet: 'https://fullnode.mainnet.sui.io:443',
testnet: 'https://fullnode.testnet.sui.io:443',
} as const;
export const dAppKit = createDAppKit({
networks: ['mainnet', 'testnet'],
defaultNetwork: 'testnet',
createClient(network) {
return new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] });
},
});
registerEnokiWallets({
apiKey: process.env.NEXT_PUBLIC_ENOKI_API_KEY!,
providers: {
google: {
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
},
},
clients: dAppKit.networks.map((network) => dAppKit.getClient(network)),
getCurrentNetwork: () => dAppKit.stores.$currentNetwork.get(),
});
In your UI, use the standard connect button:
import { useCurrentAccount, useDAppKit } from '@mysten/dapp-kit-react';
import { ConnectButton } from '@mysten/dapp-kit-react/ui';
Step 2: Build the swap transaction (client)
Build a DeepBook swap programmable transaction block (PTB) client-side, then serialize the transaction kind bytes for the sponsor.
import { deepbook } from '@mysten/deepbook-v3';
import { Transaction } from '@mysten/sui/transactions';
import { toBase64 } from '@mysten/sui/utils';
import { useCurrentAccount, useDAppKit } from '@mysten/dapp-kit-react';
const network = 'testnet' as const;
const account = useCurrentAccount();
const dAppKit = useDAppKit();
if (!account) throw new Error('Connect first');
const client = dAppKit.getClient(network).$extend(
deepbook({ address: account.address }),
);
const tx = new Transaction();
const [baseOut, quoteOut, deepOut] = tx.add(
client.deepbook.deepBook.swapExactBaseForQuote({
poolKey: 'SUI_DBUSDC',
amount: 0.1,
deepAmount: 0,
minOut: 0,
}),
);
tx.transferObjects([baseOut, quoteOut, deepOut], account.address);
tx.setSender(account.address);
const txKindBytes = toBase64(
await tx.build({ client, onlyTransactionKind: true }),
);
Step 3: Sponsor the transaction (server)
Send the serialized transaction kind bytes to your backend, which calls Enoki to create a sponsored transaction.
import { EnokiClient } from '@mysten/enoki';
const enoki = new EnokiClient({
apiKey: process.env.ENOKI_SECRET_KEY!,
});
const DEEPBOOK_PACKAGE_ID = {
testnet: '0x22be4cade64bf2d02412c7e8d0e8beea2f78828b948118d46735315409371a3c',
mainnet: '0x0e735f8c93a95722efd73521aca7a7652c0bb71ed1daf41b26dfd7d1ff71f748',
} as const;
export async function sponsorSwap({
network,
sender,
txKindBytes,
}: {
network: 'testnet' | 'mainnet';
sender: string;
txKindBytes: string;
}) {
const deepbookPackage = DEEPBOOK_PACKAGE_ID[network];
return enoki.createSponsoredTransaction({
network,
sender,
transactionKindBytes: txKindBytes,
allowedMoveCallTargets: [
`${deepbookPackage}::pool::swap_exact_base_for_quote`,
`${deepbookPackage}::pool::swap_exact_quote_for_base`,
],
allowedAddresses: [sender],
});
}
export async function executeSponsored({
digest,
signature,
}: {
digest: string;
signature: string;
}) {
return enoki.executeSponsoredTransaction({ digest, signature });
}
Step 4: Sign and execute (client)
The client receives the sponsored transaction bytes and digest from the server, signs with the wallet, and sends the signature back for execution.
const { bytes, digest } = await post('/api/sponsor-swap', {
network,
sender: account.address,
txKindBytes,
});
const { signature } = await dAppKit.signTransaction({
transaction: bytes,
network,
});
await post('/api/execute-sponsored', { digest, signature });
The server calls enoki.executeSponsoredTransaction({ digest, signature }) to submit the signed transaction.
Considerations
- Session management: Cache the ephemeral key pair and zero-knowledge proof in session storage. Users should not need to re-authenticate for every trade within a session.
- Gas budget: Enoki sets the gas budget for sponsored transactions. Monitor costs and configure per-transaction or per-user caps in the Enoki Developer Portal.
- Price slippage: DeFi transactions can fail if prices move between building and execution. Set appropriate
minOutvalues in the swap parameters. - Funding the address: The zkLogin address starts with no tokens. Users need to receive tokens before they can trade (through a fiat on-ramp, transfer from another address, or an airdrop).