Execute arbitrary smart contract calls from the private balance of the account bound to your unlink client (set via createUnlink(), see Quickstart). This enables DeFi interactions (swaps, lending, etc.) while keeping the sender private.
If you are deciding between execute() and BurnerWallet, start with DeFi.
import { buildCall, toExecuteCall } from "@unlink-xyz/sdk";
// The npk field identifies which Unlink account receives the output.
// Pass your Unlink address (bech32m); the backend derives the cryptographic
// note public key from it automatically.
const myAddress = await unlink.getAddress();
const swap = buildCall({
to: "0xDeFiRouter",
abi: "function swap(address tokenIn, address tokenOut, uint256 amount) returns (uint256)",
functionName: "swap",
args: [tokenIn, tokenOut, amount],
});
const result = await unlink.execute({
withdrawals: [{ token: "0xTokenIn", amount: "1000000000000000000" }],
calls: [toExecuteCall(swap)],
outputs: [
{ npk: myAddress, token: "0xTokenOut", min_amount: "900000000000000000" },
],
deadline: Math.floor(Date.now() / 1000) + 3600,
});
const confirmed = await unlink.pollTransactionStatus(result.txId);
Execute exposes amount, recipient, and token type on-chain (since it calls an
external contract), but keeps the sender private.
Parameters
| Parameter | Type | Required | Description |
|---|
withdrawals | Array<{ token: string; amount: string }> | Yes | Tokens to withdraw from your private balance for the calls |
calls | Array<{ to: string; data: string; value: string }> | Yes | External contract calls the adapter will execute |
outputs | Array<{ npk: string; token: string; min_amount: string }> | Yes | Expected outputs to deposit back into private balance |
deadline | number | Yes | Unix timestamp (seconds) after which the transaction reverts |
Returns: { txId: string; status: string; adapterDataHash: string }
The adapterDataHash is a binding commitment between the ZK proof and the adapter calls.
Calldata helpers
The SDK exports helpers for building the calls array. These encode ABI calls into the { to, data, value } format that execute() expects.
buildCall
Encode a call from an ABI fragment string:
import { buildCall, toExecuteCall } from "@unlink-xyz/sdk";
const call = buildCall({
to: routerAddress,
abi: "function exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) returns (uint256)",
functionName: "exactInputSingle",
args: [params],
});
// Convert to wire format for execute()
const wireCall = toExecuteCall(call);
approve
Build an ERC-20 approval call (commonly needed before a swap):
import { approve, toExecuteCall } from "@unlink-xyz/sdk";
const call = approve(usdcAddress, routerAddress, 1000000n);
const wireCall = toExecuteCall(call);
contract
Create a contract helper that encodes method calls:
import { contract, toExecuteCall } from "@unlink-xyz/sdk";
const router = contract(routerAddress, [
"function exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) returns (uint256)",
"function exactOutputSingle((address,address,uint24,address,uint256,uint256,uint160)) returns (uint256)",
]);
const call = router.exactInputSingle(params);
const wireCall = toExecuteCall(call);
toCall
Convert a transaction from ethers populateTransaction or viem simulateContract into an adapter call:
import { toCall, toExecuteCall } from "@unlink-xyz/sdk";
// ethers
const tx = await router.exactInputSingle.populateTransaction(params);
const wireCall = toExecuteCall(toCall(tx));