Execute a Swidge with Butter Network
Bind an EVM account, inspect and confirm a pinned quote, and retain transaction hashes for recovery.
Community modules are developed and maintained independently by third-party contributors.
Tether and the WDK Team do not endorse or assume responsibility for their code, security, or maintenance. Use your own judgment and proceed at your own risk.
Execute a same-chain Ethereum swap by checking prerequisites, connecting the account, reviewing a quote, and submitting the confirmed route. Review approval and retry behavior before adapting the example. For support, see Need Help?.
Prerequisites
- Complete the source snapshot installation. This guide targets source
0.2.0at revisionc1f373d, which differs from npm0.1.0. - Use Node.js 22 and an ES module application.
- Supply an Ethereum RPC endpoint, a signing account, and your Butter-issued
entrance. Butter must accept your integration and offer a route for the pair and amount. - Fund the account with the
0.001 ETHinput plus network gas. The example requests Ethereum USD₮ at its issuer-listed contract, with six decimals.
Install the EVM account module and the public RPC client in the same application:
npm install @tetherto/wdk-wallet-evm@1.0.0-beta.17 viem@2.43.1This example submits a real Ethereum transaction after interactive confirmation. Keep the private key and any API credentials in a trusted server process. Never put them in source control, browser bundles, logs, or shared shell history.
Connect the account
Set RPC_URL, PRIVATE_KEY, and BUTTER_ENTRANCE through your environment. Set MAX_NETWORK_FEE_BPS and MAX_PROTOCOL_FEE_BPS to nonnegative integer limits chosen for your application; 100 means 1%. Neither is a guaranteed ceiling on the final gas bill. See fee limits.
- Validate the environment and check that the RPC reports Ethereum chain ID
1. - Create the signing account with
WalletAccountEvm.fromPrivateKey(). - Bind the account to
ButterSwidgeProtocol, usingtoEvmPublicClient()for read operations.
Save the following JavaScript blocks, in order, in one execute-butter.mjs file. This setup uses the same RPC for the account and public client:
import ButterSwidgeProtocol, {
ButterPartialExecutionError,
formatTokenAmount,
parseTokenAmount,
toEvmPublicClient,
} from '@butternetwork/wdk-protocol-swidge-butter'
import { WalletAccountEvm } from '@tetherto/wdk-wallet-evm'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { createInterface } from 'node:readline/promises'
import { stdin, stdout } from 'node:process'
import { appendFile, writeFile } from 'node:fs/promises'
import { randomUUID } from 'node:crypto'
function requireEnv(name) {
const value = process.env[name]?.trim()
if (!value) throw new Error(`Set ${name}`)
return value
}
function feeLimit(name) {
const value = requireEnv(name)
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value))) {
throw new Error(`${name} must be a nonnegative safe integer`)
}
return Number(value)
}
const rpcUrl = requireEnv('RPC_URL')
try {
if (new URL(rpcUrl).protocol !== 'https:') throw new Error()
} catch {
throw new Error('Set a valid HTTPS RPC URL')
}
const privateKey = requireEnv('PRIVATE_KEY')
if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
throw new Error('PRIVATE_KEY must be a 32-byte 0x-prefixed hex value')
}
const entrance = requireEnv('BUTTER_ENTRANCE')
const limits = Object.freeze({
maxNetworkFeeBps: feeLimit('MAX_NETWORK_FEE_BPS'),
maxProtocolFeeBps: feeLimit('MAX_PROTOCOL_FEE_BPS'),
})
const publicClient = createPublicClient({ chain: mainnet, transport: http(rpcUrl) })
const chainId = await publicClient.getChainId().catch(() => {
throw new Error('RPC chain check failed; verify endpoint access without logging credentials')
})
if (chainId !== 1) throw new Error('RPC must use Ethereum chain 1')
let account
try {
account = WalletAccountEvm.fromPrivateKey(privateKey, { provider: rpcUrl, chainId: 1 })
} catch {
throw new Error('Could not initialize the EVM account; check the private key and provider')
}
const protocol = new ButterSwidgeProtocol(account, {
sourceChainId: '1',
entrance,
...limits,
evm: { publicClient: toEvmPublicClient(publicClient) },
})If your integration requires API credentials, configure both keys and authMode: 'required' as described in API access. Do not print the configuration object.
Review the quote
Use one immutable intent for both quoting and execution, including an explicit recipient. The example returns USD₮ to the signing account and sets maxNativeFee: 0n, so built-in EVM validation rejects additional native route fees. Ethereum network gas remains payable.
- Request a quote with
quoteSwidge(). - Display the amount, recipient, chain, output minimum, expiry, fee denominations, and configured limits.
- Require confirmation of that quote before returning its
routeHash.
Add this function to review the quote, using formatTokenAmount() only with the verified USD₮ precision:
const ETHEREUM_USDt = '0xdAC17F958D2ee523a2206206994597C13D831ec7'
const USDt_DECIMALS = 6
const json = value => JSON.stringify(value, (_key, item) => (
typeof item === 'bigint' ? item.toString() : item
), 2)
async function confirmQuote(intent) {
const quote = await protocol.quoteSwidge(intent)
console.log(json({
sourceChain: '1', intent, limits,
estimatedUSDt: formatTokenAmount(quote.toTokenAmount, USDt_DECIMALS),
minimumUSDt: formatTokenAmount(quote.toTokenAmountMin, USDt_DECIMALS),
expiryUnixSeconds: quote.expiry,
destinationGuarantees: quote.destinationGuarantees,
fees: quote.fees.map(({ type, amount, token, chain, included }) => ({
type, amountBaseUnits: amount, token, chain, included,
})),
}))
if (!stdin.isTTY || !stdout.isTTY) throw new Error('Interactive confirmation is required')
const prompt = createInterface({ input: stdin, output: stdout })
try {
if (await prompt.question('Type EXECUTE to submit this quote: ') !== 'EXECUTE') {
throw new Error('Execution was not confirmed')
}
} finally {
prompt.close()
}
return quote
}Fee amounts above are base units of each entry's token on its chain. Do not sum different assets. Missing fee data or a zero placeholder does not prove that an operation is free. Quote generation does not enforce the percentage caps; execution does.
The same-chain built-in EVM path reports destinationGuarantees: 'enforced' and checks the minimum in Router calldata. Cross-chain and adapter execution report quoted-only; do not present their quoted minimum as a destination guarantee enforced by this package.
Submit the confirmed route
Keep the same instance and unchanged intent when calling swidge(). A pin is consumed by one execution attempt, including attempts that fail later. It is not an idempotency key. A stale, mismatched, or consumed pin fails without silently selecting another quote.
- Create a local operation record before quoting.
- Obtain confirmation and submit with that quote's
routeHash. - Save the source hash and every returned transaction. On a known partial failure, save all available hashes before stopping.
- Release the signing account with
dispose().
Add this execution block. ButterPartialExecutionError exposes transactions that the module knows were broadcast:
const recordPath = `butter-${randomUUID()}.jsonl`
async function recordOperation(record) {
const line = json(record).replaceAll('\n', '') + '\n'
try {
await appendFile(recordPath, line, { encoding: 'utf8', flush: true })
} catch {
console.error('Operation record write failed. Retain these details:', line)
throw new Error('Inspect the wallet and preserve operation details before retrying')
}
}
try {
const intent = Object.freeze({
fromToken: 'native',
toToken: ETHEREUM_USDt,
fromTokenAmount: parseTokenAmount('0.001', 18),
toChain: '1',
recipient: await account.getAddress(),
slippage: 0.01,
maxNativeFee: 0n,
})
await writeFile(recordPath, '', { flag: 'wx', mode: 0o600 })
await recordOperation({ state: 'created', fromChain: '1', toChain: '1', intent })
console.log('Operation record:', recordPath)
const quote = await confirmQuote(intent)
try {
const result = await protocol.swidge({ ...intent, routeHash: quote.routeHash })
await recordOperation({
state: 'submitted', sourceHash: result.id, fromChain: '1', toChain: '1',
transactions: result.transactions,
})
console.log('Source transaction:', result.id)
} catch (error) {
if (error instanceof ButterPartialExecutionError) {
await recordOperation({
state: 'partial', fromChain: '1', toChain: '1',
transactions: error.transactions, failedType: error.failedType,
})
}
throw new Error('Execution did not finish. Inspect the record and wallet before retrying')
}
} catch {
console.error('Operation stopped. Inspect the operation record and wallet before retrying.')
process.exitCode = 1
} finally {
account.dispose()
}Run the assembled file in an interactive terminal after setting the environment:
node execute-butter.mjsSubmission returns the source transaction hash, not final settlement. Returned output amounts remain estimates. Continue with Track Settlement.
The local record contains hashes reported after submission returns or throws. A process crash or sender failure can leave a broadcast without a recorded hash. Production applications also need durable wallet submission records and RPC reconciliation; absence of a partial-error wrapper does not establish that nothing was sent.
Handle approvals and retries
The native ETH example needs no token approval. For ERC-20 inputs, built-in EVM execution uses the validated Router as spender and requires an allowance reader and approval receipt support:
- An allowance equal to the exact input amount requires no approval.
- A zero allowance requires an exact-amount approval.
- Any other nonzero allowance, including a larger allowance, requires a confirmed zero reset followed by a confirmed exact-amount approval.
The module checks the exact allowance after each approval. An operation can submit two approval transactions and one source transaction. Approval timeout does not undo a broadcast, and route freshness is checked again before each send.
After any uncertain failure, inspect all known transaction hashes, account nonces, receipts, and current allowance before requesting a fresh quote. Do not automatically call swidge() again. The package does not revoke approvals, refund funds, or roll back submitted transactions.
For cross-chain EVM execution, choose an explicit absolute native-fee bound suitable for the route; maxNativeFee excludes network gas. Host adapters must enforce their own native-spend and transaction-intent bounds. Review Configuration before changing the chain, token, recipient, or adapter.
Next Steps
Track Settlement
Resume status checks from a source hash and handle partial execution.
Configuration
Review fee boundaries, approval settings, and adapters.
API Reference
Inspect execution options, return values, and errors.