Skip to main content

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 minOut values 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).