feat(did): add web auth for oidc authorization flows

This commit is contained in:
2026-01-05 13:57:11 -05:00
parent e0ee87565d
commit 28a3f2b952
2 changed files with 85 additions and 719 deletions

View File

@@ -1,719 +0,0 @@
<!DOCTYPE html>
<!--
================================================================================
TEMPL MIGRATION GUIDE: authorize.html → views/authorize.templ
================================================================================
PAGE OVERVIEW:
- OAuth/OIDC consent screen for third-party app authorization
- Three request types via tabs: Connect, Sign Message, Transaction
- Displays requesting app identity with verification badge
- Shows wallet selector and permission scopes
- Transaction tab includes swap preview, gas estimates, contract data
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
- Card max-width: 48ch (~384px) fits all viewport sizes
- Tab panels scroll independently if content exceeds viewport
- Transaction details use compact layout on small screens
- Approve/Deny buttons always visible in footer
MAIN TEMPL COMPONENT:
templ AuthorizePage(req AuthRequest, wallet WalletInfo) {
@layouts.CenteredCard("Authorize - Sonr Motr Wallet") {
@AppIdentityHeader(req.App)
<wa-divider></wa-divider>
@RequestTypeTabs(req)
@AuthFooterActions(req.Type)
}
}
HTMX INTEGRATION:
- Tab switching: hx-on:wa-tab-show="htmx.ajax('GET', '/authorize/tab/' + event.detail.name, '#tab-content')"
- Approve action: hx-post="/api/authorize/approve" hx-include="[name='scopes']" hx-target="#auth-result"
- Deny action: hx-post="/api/authorize/deny" hx-target="#auth-result"
- Wallet selector: hx-get="/authorize/wallets" hx-target="#wallet-dropdown" hx-trigger="click"
SUB-COMPONENTS TO EXTRACT:
- AppIdentityHeader(app AppInfo) // Logo, name, verified badge, domain
- WalletSelector(wallets []Wallet, selected string)
- PermissionItem(icon string, iconClass string, title string, desc string)
- PermissionList(permissions []Permission)
- MessagePreview(message string, signType string)
- TransactionPreview(tx Transaction)
- SwapPreview(from TokenAmount, to TokenAmount)
- GasEstimate(network string, estimatedGas string, maxFee string)
- ContractDataDetails(contract string, function string, data string)
- AuthFooterActions(requestType string)
- AuthResultSuccess(type string, txHash string)
- AuthResultDenied()
STATE/PROPS:
type AuthRequest struct {
Type string // "connect", "sign", "transaction"
App AppInfo
Scopes []string
Message string // For sign requests
Transaction *TxDetails // For transaction requests
State string // OAuth state param
Nonce string // For OIDC
}
type AppInfo struct {
Name string
Domain string
LogoURL string
Verified bool
}
type TxDetails struct {
Type string // "swap", "send", "contract"
FromToken TokenAmount
ToToken TokenAmount
Network string
EstimatedGas string
MaxFee string
Contract string
Function string
RawData string
}
HTMX PATTERNS:
// Tab group with HTMX partial loading
<wa-tab-group hx-on:wa-tab-show="
htmx.ajax('GET', '/authorize/panel/' + event.detail.name, {target: '#panel-content'});
htmx.ajax('GET', '/authorize/button/' + event.detail.name, {target: '#approve-btn', swap: 'outerHTML'});
">
// Approve button with loading state
<wa-button id="approve-btn" variant="brand"
hx-post="/api/authorize/approve"
hx-include="[name='scopes'],[name='wallet']"
hx-target="#auth-card"
hx-indicator="this">
// Wallet selector dropdown via HTMX
<div class="wallet-selector"
hx-get="/authorize/wallet-options"
hx-target="#wallet-dropdown"
hx-trigger="click">
// Out-of-band result update (closes modal/redirects)
templ AuthResult(success bool, redirectURL string) {
<div id="auth-card" hx-swap-oob="true">
if success {
@AuthResultSuccess()
<script>setTimeout(() => window.location.href = "{ redirectURL }", 2000)</script>
} else {
@AuthResultDenied()
}
</div>
}
================================================================================
-->
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Allow Access - Sonr</title>
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
<style>
:root {
--wa-color-primary: #17c2ff;
}
html, body {
min-height: 100%;
padding: 0;
margin: 0;
}
.main-centered {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
padding: var(--wa-space-l);
box-sizing: border-box;
}
.main-centered wa-card {
width: 100%;
max-width: 48ch;
}
/* App identity header */
.app-identity {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--wa-space-s);
text-align: center;
padding-bottom: var(--wa-space-m);
}
.app-logo {
width: 64px;
height: 64px;
border-radius: var(--wa-radius-l);
display: flex;
align-items: center;
justify-content: center;
background: var(--wa-color-surface-alt);
border: 1px solid var(--wa-color-neutral-200);
}
.app-logo img {
width: 48px;
height: 48px;
border-radius: var(--wa-radius-m);
}
.app-name {
display: flex;
align-items: center;
gap: var(--wa-space-2xs);
}
.verified-badge {
color: var(--wa-color-primary);
}
/* Wallet selector */
.wallet-selector {
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-m);
padding: var(--wa-space-s) var(--wa-space-m);
}
.wallet-address {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-s);
}
/* Permission items */
.permission-item {
display: flex;
align-items: flex-start;
gap: var(--wa-space-s);
padding: var(--wa-space-s) 0;
}
.permission-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: var(--wa-radius-s);
display: flex;
align-items: center;
justify-content: center;
}
.permission-icon.read {
background: var(--wa-color-success-subtle);
color: var(--wa-color-success);
}
.permission-icon.write {
background: var(--wa-color-warning-subtle);
color: var(--wa-color-warning);
}
.permission-icon.sign {
background: var(--wa-color-primary-subtle);
color: var(--wa-color-primary);
}
.permission-icon.danger {
background: var(--wa-color-danger-subtle);
color: var(--wa-color-danger);
}
/* Transaction preview */
.tx-preview {
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-m);
padding: var(--wa-space-m);
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-s);
word-break: break-all;
max-height: 120px;
overflow-y: auto;
}
.tx-detail-row {
display: flex;
justify-content: space-between;
padding: var(--wa-space-xs) 0;
}
.tx-detail-row:not(:last-child) {
border-bottom: 1px solid var(--wa-color-neutral-200);
}
.tx-label {
color: var(--wa-color-neutral-600);
}
.tx-value {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-s);
}
/* Actions footer */
.auth-actions {
display: flex;
gap: var(--wa-space-s);
padding-top: var(--wa-space-m);
}
.auth-actions wa-button {
flex: 1;
}
/* Tabs for different request types */
.request-tabs wa-tab-panel {
padding-top: var(--wa-space-m);
}
/* Raw data toggle */
.raw-data-section {
margin-top: var(--wa-space-m);
}
.raw-data-content {
background: var(--wa-color-neutral-900);
color: var(--wa-color-neutral-100);
border-radius: var(--wa-radius-m);
padding: var(--wa-space-m);
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
word-break: break-all;
max-height: 150px;
overflow-y: auto;
margin-top: var(--wa-space-s);
}
/* Viewport constraints for popup/webview (360-480px width) */
@media (max-width: 400px) {
.main-centered {
padding: var(--wa-space-m);
}
.main-centered wa-card {
max-width: 100%;
}
.app-logo {
width: 48px;
height: 48px;
}
.app-logo img {
width: 36px;
height: 36px;
}
.permission-icon {
width: 28px;
height: 28px;
}
.wallet-selector {
padding: var(--wa-space-xs) var(--wa-space-s);
}
.tx-preview {
padding: var(--wa-space-s);
max-height: 100px;
}
.raw-data-content {
padding: var(--wa-space-s);
max-height: 100px;
}
}
@media (max-height: 600px) {
.app-identity {
padding-bottom: var(--wa-space-s);
}
.app-logo {
width: 48px;
height: 48px;
}
.permission-item {
padding: var(--wa-space-xs) 0;
}
.tx-preview {
max-height: 80px;
}
}
</style>
</head>
<body>
<wa-page>
<main class="main-centered">
<wa-card>
<!-- App Identity Header -->
<div class="app-identity">
<div class="app-logo">
<wa-icon name="cube" style="font-size: 32px; color: var(--wa-color-primary);"></wa-icon>
</div>
<div class="wa-stack wa-gap-2xs" style="align-items: center;">
<div class="app-name">
<span class="wa-heading-l">Uniswap</span>
<wa-icon name="badge-check" variant="solid" class="verified-badge"></wa-icon>
</div>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">app.uniswap.org</span>
</div>
</div>
<wa-divider></wa-divider>
<!-- Request Type Tabs -->
<wa-tab-group class="request-tabs">
<wa-tab panel="connect">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="link"></wa-icon>
<span>Connect</span>
</div>
</wa-tab>
<wa-tab panel="sign">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="pen-nib"></wa-icon>
<span>Sign</span>
</div>
</wa-tab>
<wa-tab panel="transaction">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="paper-plane"></wa-icon>
<span>Transaction</span>
</div>
</wa-tab>
<!-- Connect Panel -->
<wa-tab-panel name="connect">
<div class="wa-stack">
<!-- Wallet Selection -->
<div class="wallet-selector">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="S" style="--size: 36px;"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Main Wallet</span>
<span class="wallet-address">sonr1x9f...7k2m</span>
</div>
</div>
<wa-icon-button name="chevron-down" variant="plain"></wa-icon-button>
</div>
</div>
<!-- Permissions List -->
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-s">This app wants to:</span>
<div class="permission-item">
<div class="permission-icon read">
<wa-icon name="eye"></wa-icon>
</div>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">See your wallet address</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Your public address only</span>
</div>
</div>
<div class="permission-item">
<div class="permission-icon read">
<wa-icon name="coins"></wa-icon>
</div>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">See your balances</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Token amounts in your wallet</span>
</div>
</div>
<div class="permission-item">
<div class="permission-icon write">
<wa-icon name="bell"></wa-icon>
</div>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">Ask to send transactions</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">You'll approve each one separately</span>
</div>
</div>
</div>
<!-- Warning Callout -->
<wa-callout variant="neutral">
<wa-icon slot="icon" name="shield-check"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">This app cannot:</span>
<span class="wa-caption-s">Spend your funds without asking you first.</span>
</div>
</wa-callout>
</div>
</wa-tab-panel>
<!-- Sign Message Panel -->
<wa-tab-panel name="sign">
<div class="wa-stack">
<!-- Signing Wallet -->
<div class="wallet-selector">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="S" style="--size: 36px;"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Main Wallet</span>
<span class="wallet-address">sonr1x9f...7k2m</span>
</div>
</div>
<wa-badge variant="neutral">Signing</wa-badge>
</div>
</div>
<!-- Message Preview -->
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-s">You're signing this message:</span>
<div class="tx-preview">
Welcome to Uniswap!
Click to sign in and accept the Uniswap Terms of Service.
This request will not trigger a blockchain transaction or cost any gas fees.
Wallet address:
sonr1x9f4h2k8m3n5p7q2r4s6t8v0w3x5y7z9a1b3c5d7k2m
Nonce: 8f4a2b1c
</div>
</div>
<!-- Sign Type Info -->
<wa-callout variant="brand" appearance="filled">
<wa-icon slot="icon" name="signature"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Free to sign</span>
<span class="wa-caption-s">No fees. This just proves you own this wallet.</span>
</div>
</wa-callout>
<!-- Advanced: Raw Data Toggle -->
<div class="raw-data-section">
<wa-details summary="Show technical details">
<div class="raw-data-content">
0x57656c636f6d6520746f20556e697377617021
436c69636b20746f207369676e20696e20616e642061636365707420746865
20556e6973776170205465726d73206f6620536572766963652e0a0a5468
69732072657175657374200a0a57616c6c65742061646472657373
</div>
</wa-details>
</div>
</div>
</wa-tab-panel>
<!-- Transaction Panel -->
<wa-tab-panel name="transaction">
<div class="wa-stack">
<!-- From Wallet -->
<div class="wallet-selector">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="S" style="--size: 36px;"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Main Wallet</span>
<span class="wallet-address">sonr1x9f...7k2m</span>
</div>
</div>
<div class="wa-stack wa-gap-0" style="text-align: right;">
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Balance</span>
<span class="wa-heading-s">1,234.56 SNR</span>
</div>
</div>
</div>
<!-- Transaction Details Card -->
<wa-card>
<div class="wa-stack wa-gap-s">
<div class="wa-flank">
<span class="wa-heading-m">Swap</span>
<wa-badge variant="warning">Waiting</wa-badge>
</div>
<wa-divider></wa-divider>
<!-- Swap Visual -->
<div class="wa-stack wa-gap-s">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="E" style="--size: 32px; background: var(--wa-color-neutral-200);"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">100.00 ETH</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Ethereum</span>
</div>
</div>
<span class="wa-caption-s" style="color: var(--wa-color-danger);">-$234,567.00</span>
</div>
<div style="text-align: center;">
<wa-icon name="arrow-down" style="color: var(--wa-color-neutral-400);"></wa-icon>
</div>
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="U" style="--size: 32px; background: var(--wa-color-primary-subtle);"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">~125,000 USDC</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">USD Coin</span>
</div>
</div>
<span class="wa-caption-s" style="color: var(--wa-color-success);">+$125,000.00</span>
</div>
</div>
</div>
</wa-card>
<!-- Gas & Fee Details -->
<div class="wa-stack wa-gap-2xs">
<div class="tx-detail-row">
<span class="tx-label">Network</span>
<span class="tx-value">Sonr Mainnet</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Network fee</span>
<span class="tx-value">~$0.12</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Max fee</span>
<span class="tx-value">$0.26</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Price change limit</span>
<span class="tx-value">0.5%</span>
</div>
</div>
<!-- Danger Warning for High Value -->
<wa-callout variant="warning">
<wa-icon slot="icon" name="triangle-alert"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Large amount</span>
<span class="wa-caption-s">Please review the details carefully before confirming.</span>
</div>
</wa-callout>
<!-- Advanced: Contract Data -->
<div class="raw-data-section">
<wa-details summary="Show technical details">
<div class="wa-stack wa-gap-s">
<div class="tx-detail-row">
<span class="tx-label">Contract</span>
<span class="tx-value">0x7a25...3f8b</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Function</span>
<span class="tx-value">swapExactTokensForTokens()</span>
</div>
<div class="raw-data-content">
0x38ed1739
0000000000000000000000000000000000000056bc75e2d63100000
000000000000000000000000000000000000001e8480
00000000000000000000000000000000000000000000000000000060
0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659
</div>
</div>
</wa-details>
</div>
</div>
</wa-tab-panel>
</wa-tab-group>
<!-- Footer Actions -->
<div slot="footer">
<div class="wa-stack wa-gap-s">
<div class="auth-actions">
<wa-button variant="neutral" appearance="outlined" id="deny-btn">
<wa-icon slot="start" name="x"></wa-icon>
Cancel
</wa-button>
<wa-button variant="brand" id="approve-btn">
<wa-icon slot="start" name="check"></wa-icon>
Allow
</wa-button>
</div>
<div style="text-align: center;">
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
<wa-icon name="lock" style="font-size: 12px;"></wa-icon>
Protected by Sonr
</span>
</div>
</div>
</div>
</wa-card>
</main>
</wa-page>
<script>
// Tab panel state management
const tabGroup = document.querySelector('wa-tab-group');
const approveBtn = document.getElementById('approve-btn');
const denyBtn = document.getElementById('deny-btn');
// Update button text based on active panel
tabGroup.addEventListener('wa-tab-show', (event) => {
const panel = event.detail.name;
switch (panel) {
case 'connect':
approveBtn.innerHTML = '<wa-icon slot="start" name="link"></wa-icon>Allow';
break;
case 'sign':
approveBtn.innerHTML = '<wa-icon slot="start" name="pen-nib"></wa-icon>Sign';
break;
case 'transaction':
approveBtn.innerHTML = '<wa-icon slot="start" name="paper-plane"></wa-icon>Confirm';
break;
}
});
// Simulated approve action
approveBtn.addEventListener('click', async () => {
const activePanel = tabGroup.querySelector('wa-tab[active]')?.getAttribute('panel') || 'connect';
approveBtn.loading = true;
approveBtn.disabled = true;
denyBtn.disabled = true;
// Simulate WebAuthn authentication
await new Promise(resolve => setTimeout(resolve, 1500));
// Show success
const card = document.querySelector('wa-card');
card.innerHTML = `
<div class="wa-stack" style="text-align: center; padding: var(--wa-space-xl) 0;">
<wa-icon name="circle-check" variant="solid" style="font-size: 64px; color: var(--wa-color-success);"></wa-icon>
<span class="wa-heading-l">${activePanel === 'connect' ? 'Connected' : activePanel === 'sign' ? 'Signed' : 'Sent'}</span>
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
${activePanel === 'transaction' ? 'Transaction ID: 0x8f4a2b1c...' : 'You can close this window.'}
</span>
<wa-button variant="neutral" appearance="outlined" onclick="window.close()">
Close
</wa-button>
</div>
`;
});
// Deny action
denyBtn.addEventListener('click', () => {
const card = document.querySelector('wa-card');
card.innerHTML = `
<div class="wa-stack" style="text-align: center; padding: var(--wa-space-xl) 0;">
<wa-icon name="circle-x" variant="solid" style="font-size: 64px; color: var(--wa-color-danger);"></wa-icon>
<span class="wa-heading-l">Cancelled</span>
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
No access was granted.
</span>
<wa-button variant="neutral" appearance="outlined" onclick="window.close()">
Close
</wa-button>
</div>
`;
});
</script>
</body>
</html>

View File

@@ -20,6 +20,12 @@ func RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /login", handleLogin) mux.HandleFunc("GET /login", handleLogin)
mux.HandleFunc("GET /login/step/{step}", handleLoginStep) mux.HandleFunc("GET /login/step/{step}", handleLoginStep)
mux.HandleFunc("GET /login/qr-status", handleLoginQRStatus) mux.HandleFunc("GET /login/qr-status", handleLoginQRStatus)
mux.HandleFunc("GET /authorize", handleAuthorize)
mux.HandleFunc("POST /authorize/approve", handleAuthorizeApprove)
mux.HandleFunc("POST /authorize/deny", handleAuthorizeDeny)
mux.HandleFunc("GET /dashboard", handleDashboard)
} }
// handleWelcome renders the full welcome page at step 1 // handleWelcome renders the full welcome page at step 1
@@ -122,3 +128,82 @@ func handleLoginQRStatus(w http.ResponseWriter, r *http.Request) {
} }
views.QRStatusWaiting().Render(r.Context(), w) views.QRStatusWaiting().Render(r.Context(), w)
} }
func handleAuthorize(w http.ResponseWriter, r *http.Request) {
reqType := r.URL.Query().Get("type")
if reqType == "" {
reqType = "connect"
}
req := views.AuthRequest{
Type: reqType,
App: views.AppInfo{
Name: "Uniswap",
Domain: "app.uniswap.org",
LogoIcon: "cube",
Verified: true,
},
Wallet: views.WalletInfo{
Name: "Main Wallet",
Address: "sonr1x9f...7k2m",
Balance: "1,234.56 SNR",
},
Message: `Welcome to Uniswap!
Click to sign in and accept the Uniswap Terms of Service.
This request will not trigger a blockchain transaction or cost any gas fees.
Wallet address:
sonr1x9f4h2k8m3n5p7q2r4s6t8v0w3x5y7z9a1b3c5d7k2m
Nonce: 8f4a2b1c`,
MessageHex: "0x57656c636f6d6520746f20556e697377617021...",
Transaction: &views.TxDetails{
Type: "Swap",
FromToken: views.TokenAmount{
Symbol: "ETH",
Amount: "100.00",
USD: "$234,567.00",
Initials: "E",
},
ToToken: views.TokenAmount{
Symbol: "USDC",
Amount: "125,000",
USD: "$125,000.00",
Initials: "U",
},
Network: "Sonr Mainnet",
NetworkFee: "~$0.12",
MaxFee: "$0.26",
Slippage: "0.5%",
Contract: "0x7a25...3f8b",
Function: "swapExactTokensForTokens()",
RawData: "0x38ed1739\n0000000000000000000000000000000000000056bc75e2d63100000...",
},
}
views.AuthorizePage(req).Render(r.Context(), w)
}
func handleAuthorizeApprove(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
actionType := r.FormValue("type")
if actionType == "" {
actionType = "connect"
}
views.AuthResultSuccess(actionType).Render(r.Context(), w)
}
func handleAuthorizeDeny(w http.ResponseWriter, r *http.Request) {
views.AuthResultDenied().Render(r.Context(), w)
}
func handleDashboard(w http.ResponseWriter, r *http.Request) {
tab := r.URL.Query().Get("tab")
if tab == "" {
tab = "accounts"
}
data := views.DefaultDashboardData()
views.DashboardPage(data, tab).Render(r.Context(), w)
}