Complete Vue Example
This is a complete, production-ready example of a Silent Swap implementation using Vue 3 composables.
Full Component
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { getSilentSwapClient, getSilentSwapOrders, getSilentSwapAuth } from '@silentswap/vue';
import { useAccount, useSignMessage, useSignTypedData } from 'wagmi';
import {
ENVIRONMENT,
DeliveryMethod,
caip19FungibleEvmToken,
FacilitatorKeyType,
PublicKeyArgGroups,
createHdFacilitatorGroupFromEntropy,
} from '@silentswap/sdk';
import { hexToBytes } from 'viem';
const { address, isConnected } = useAccount();
const { signMessageAsync } = useSignMessage();
const { signTypedDataAsync } = useSignTypedData();
// Initialize client
const { client } = getSilentSwapClient({
config: {
environment: ENVIRONMENT.MAINNET,
baseUrl: 'https://api.silentswap.com',
},
});
// Get composables
const {
isLoading,
error,
getNonce,
authenticate,
getQuote,
createOrder,
} = getSilentSwapOrders({ client });
const {
createSignInMessage,
createEip712DocForOrder,
createEip712DocForWalletGeneration,
} = getSilentSwapAuth();
// State
const authResult = ref<any>(null);
const walletEntropy = ref<Uint8Array | null>(null);
const facilitatorGroup = ref<any>(null);
const quote = ref<any>(null);
const order = ref<any>(null);
// Form state
const sourceAmount = ref('');
const destinationAddress = ref('');
// Authentication flow
const handleSignIn = async () => {
if (!address.value) return;
try {
// Get nonce
const nonceResponse = await getNonce(address.value);
if (!nonceResponse) {
throw new Error('Failed to get nonce');
}
// Create SIWE message
const message = createSignInMessage(
address.value,
nonceResponse.nonce,
window.location.host
);
// Sign message
const signature = await signMessageAsync({ message });
// Authenticate
const result = await authenticate({
siwe: {
message,
signature: signature as `0x${string}`,
},
});
if (!result) {
throw new Error('Authentication failed');
}
authResult.value = result;
console.log('Authenticated:', result.address);
// Generate wallet entropy
await generateWalletEntropy(result.secretToken);
} catch (err) {
console.error('Sign in failed:', err);
}
};
// Generate wallet entropy
const generateWalletEntropy = async (secretToken: string) => {
if (!address.value) return;
try {
const walletDoc = createEip712DocForWalletGeneration(
`eip155:43114:${address.value}`,
secretToken
);
const signature = await signTypedDataAsync(walletDoc);
walletEntropy.value = hexToBytes(signature as `0x${string}`);
// Create facilitator group
const group = await createHdFacilitatorGroupFromEntropy(
walletEntropy.value,
0 // nonce
);
facilitatorGroup.value = group;
console.log('Wallet entropy generated');
} catch (err) {
console.error('Wallet generation failed:', err);
}
};
// Get quote
const handleGetQuote = async () => {
if (!facilitatorGroup.value || !sourceAmount.value || !destinationAddress.value) {
return;
}
try {
const viewer = await facilitatorGroup.value.viewer();
const { publicKeyBytes: viewerPk } = viewer.exportPublicKey('*', FacilitatorKeyType.SECP256K1);
const groupPks = await facilitatorGroup.value.exportPublicKeys(1, PublicKeyArgGroups.GENERIC);
const quoteResult = await getQuote({
signer: address.value!,
viewer: viewerPk,
outputs: [{
method: DeliveryMethod.SNIP,
asset: caip19FungibleEvmToken(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'),
value: (parseFloat(sourceAmount.value) * 1e6).toString() as `${bigint}`,
recipient: destinationAddress.value as `0x${string}`,
facilitatorPublicKeys: groupPks[0],
}],
});
if (quoteResult) {
quote.value = quoteResult;
console.log('Quote received:', quoteResult.quoteId);
}
} catch (err) {
console.error('Quote failed:', err);
}
};
// Create order
const handleCreateOrder = async () => {
if (!quote.value || !facilitatorGroup.value) {
return;
}
try {
// Create EIP-712 document
const orderDoc = createEip712DocForOrder(quote.value);
// Sign the document
const signature = await signTypedDataAsync(orderDoc);
// Approve proxy authorizations
const facilitatorReplies = await facilitatorGroup.value.approveProxyAuthorizations(
quote.value.facilitators,
{
proxyPublicKey: client.proxyPublicKey,
}
);
// Create order
const orderResult = await createOrder({
quote: quote.value.quote,
quoteId: quote.value.quoteId,
authorizations: [], // You'd sign these separately based on quote.authorizations
eip712Domain: orderDoc.domain,
signature,
facilitators: facilitatorReplies,
});
if (orderResult) {
order.value = orderResult;
console.log('Order created:', orderResult.response.orderId);
}
} catch (err) {
console.error('Order creation failed:', err);
}
};
// Computed properties
const isAuthenticated = computed(() => authResult.value !== null);
const isWalletReady = computed(() => facilitatorGroup.value !== null);
const canGetQuote = computed(() =>
isWalletReady.value &&
sourceAmount.value &&
parseFloat(sourceAmount.value) > 0 &&
destinationAddress.value
);
const canCreateOrder = computed(() => quote.value !== null && isWalletReady.value);
// Watch for errors
watch(error, (err) => {
if (err) {
console.error('Error:', err.message);
}
});
</script>
<template>
<div class="swap-container">
<h1>SilentSwap - Vue Integration</h1>
<!-- Connection Status -->
<div v-if="!isConnected" class="status">
<p>Please connect your wallet</p>
</div>
<!-- Authentication -->
<div v-else-if="!isAuthenticated" class="auth-section">
<h2>Authentication</h2>
<button
@click="handleSignIn"
:disabled="isLoading"
class="btn-primary"
>
{{ isLoading ? 'Signing In...' : 'Sign In with Ethereum' }}
</button>
</div>
<!-- Main Swap Form -->
<div v-else class="swap-form">
<div v-if="!isWalletReady" class="status">
<p>Generating wallet...</p>
</div>
<div v-else>
<!-- Error Display -->
<div v-if="error" class="error">
Error: {{ error.message }}
</div>
<!-- Source Amount -->
<div class="form-group">
<label>Source Amount</label>
<input
v-model="sourceAmount"
type="number"
placeholder="0.0"
step="0.000001"
/>
</div>
<!-- Destination Address -->
<div class="form-group">
<label>Destination Address</label>
<input
v-model="destinationAddress"
type="text"
placeholder="0x..."
/>
</div>
<!-- Actions -->
<div class="actions">
<button
@click="handleGetQuote"
:disabled="!canGetQuote || isLoading"
class="btn-secondary"
>
{{ isLoading ? 'Getting Quote...' : 'Get Quote' }}
</button>
<button
@click="handleCreateOrder"
:disabled="!canCreateOrder || isLoading"
class="btn-primary"
>
{{ isLoading ? 'Creating Order...' : 'Create Order' }}
</button>
</div>
<!-- Quote Display -->
<div v-if="quote" class="quote-details">
<h3>Quote Details</h3>
<div class="detail-row">
<span>Quote ID:</span>
<code>{{ quote.quoteId }}</code>
</div>
<div class="detail-row">
<span>Authorizations:</span>
<span>{{ quote.authorizations.length }}</span>
</div>
<div class="detail-row">
<span>Facilitators:</span>
<span>{{ quote.facilitators.length }}</span>
</div>
</div>
<!-- Order Display -->
<div v-if="order" class="order-details">
<h3>Order Created</h3>
<div class="detail-row">
<span>Order ID:</span>
<code>{{ order.response.orderId }}</code>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.swap-container {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.status {
text-align: center;
padding: 1rem;
background: #f0f0f0;
border-radius: 8px;
margin-bottom: 1rem;
}
.error {
padding: 1rem;
background: #fee;
border: 1px solid #fcc;
border-radius: 8px;
margin-bottom: 1rem;
color: #c00;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.actions {
display: flex;
gap: 1rem;
margin: 1.5rem 0;
}
.btn-primary,
.btn-secondary {
flex: 1;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-primary:disabled,
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.quote-details,
.order-details {
margin-top: 2rem;
padding: 1rem;
background: #f9f9f9;
border-radius: 8px;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
.detail-row:last-child {
border-bottom: none;
}
code {
font-family: monospace;
background: #f0f0f0;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.9em;
}
</style>Key Features
- Reactive State - Uses Vue's reactivity system for automatic UI updates
- Complete Flow - Handles authentication, wallet generation, quotes, and orders
- Error Handling - Comprehensive error handling with user feedback
- Type Safety - Full TypeScript support
- Computed Properties - Smart computed values for form validation
Flow Summary
- Connect Wallet - User connects wallet via wagmi
- Authenticate - Sign in with Ethereum (SIWE)
- Generate Wallet - Create facilitator wallet from entropy
- Get Quote - Fetch quote with facilitator public keys
- Create Order - Sign and submit order
Next Steps
- Learn about client initialization
- Understand authentication utilities
- Explore order management