diff --git a/README.md b/README.md index ad77f11..582630a 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ nimiq-tutorial/ │ │ ├── 1-connecting-to-network/ │ │ ├── 2-working-with-transactions/ │ │ ├── 3-staking-and-validators/ -│ │ └── 4-miscellaneous/ +│ │ └── 4-polygon/ │ └── templates/ # Code templates ├── public/ # Static assets ├── scripts/ # Build scripts diff --git a/astro.config.ts b/astro.config.ts index 38b9041..d02469c 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -1,9 +1,9 @@ +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' import vue from '@astrojs/vue' import tutorialkit from '@tutorialkit/astro' import { defineConfig } from 'astro/config' -import { readFileSync } from 'fs' -import { fileURLToPath } from 'url' -import path from 'path' // Read package version at build time const __filename = fileURLToPath(import.meta.url) diff --git a/eslint.config.js b/eslint.config.js index efcd7a1..40c71d2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,7 +4,7 @@ export default antfu({ formatters: true, unocss: true, astro: true, - ignores: ['./public/widget.css', './public/widget.js'], + ignores: ['./public/widget.css', './public/widget.js', './src/components/CustomTopBar.astro'], }, { rules: { 'no-console': 'off', diff --git a/public/widget.css b/public/widget.css index 96c3387..bda0e73 100644 --- a/public/widget.css +++ b/public/widget.css @@ -1 +1 @@ -[data-app=nimiq-pay] [data-input=share-debug-info]{display:inherit!important}[nq-input-box]{max-height:calc(20lh + 2 * var(--padding));border-radius:6px;padding:10px 12px;border:none;--border-color: var(--colors-neutral-400);outline:1.5px solid var(--border-color)}[nq-input-box]:placeholder{--placeholder-color: var(--colors-neutral-500);color:var(--placeholder-color);transition:color .2s var(--nq-ease)}[nq-input-box]:hover{--border-color: var(--colors-blue-600)}[nq-input-box]:focus,[nq-input-box]:focus-visible{--border-color: var(--colors-blue);color:var(--colors-blue);outline-style:solid;outline-width:1.5px}[nq-pill-xl]{border-radius:9999px;color:var(--colors-white);width:100%;padding:1.5lh;line-height:1;font-weight:600;text-transform:uppercase;letter-spacing:.1em;font-size:15px}.grid-container button{@property --un-text-opacity{syntax:"";inherits:false;initial-value:100%;}}.grid-container button[data-v-3e19f25c]{color:color-mix(in srgb,var(--colors-white) var(--un-text-opacity),transparent);--un-border-style:none;border-style:none;display:flex;flex-direction:column;gap:calc(var(--spacing) * 8);cursor:pointer;align-items:center;justify-content:center;--f-text-min:14;--f-text-max:16;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)));--f-p-min:24;--f-p-max:32;padding:clamp(calc(var(--f-p-unit, 1px) * var(--f-p-min, 16)),calc(var(--f-p-unit, 1px) * var(--f-p-min, 16) + (var(--f-p-max, 16) - var(--f-p-min, 16)) * (var(--f-p-container, 100vw) - (var(--f-p-unit, 1px) * var(--f-p-min-container, 320))) / (var(--f-p-max-container, 1920) - var(--f-p-min-container, 320))),calc(var(--f-p-unit, 1px) * var(--f-p-max, 16)));--f-rounded-min:6;--f-rounded-max:8;border-radius:clamp(calc(var(--f-rounded-unit, 1px) * var(--f-rounded-min, 16)),calc(var(--f-rounded-unit, 1px) * var(--f-rounded-min, 16) + (var(--f-rounded-max, 16) - var(--f-rounded-min, 16)) * (var(--f-rounded-container, 100vw) - (var(--f-rounded-unit, 1px) * var(--f-rounded-min-container, 320))) / (var(--f-rounded-max-container, 1920) - var(--f-rounded-min-container, 320))),calc(var(--f-rounded-unit, 1px) * var(--f-rounded-max, 16)))}@supports (color: color-mix(in lab,red,red)){.grid-container button[data-v-3e19f25c]{color:color-mix(in oklab,var(--colors-white) var(--un-text-opacity),transparent)}}.grid-container button>div[data-v-3e19f25c]:first-child{--f-size-min:24;--f-size-max:32;width:clamp(calc(var(--f-size-unit, 1px) * var(--f-size-min, 16)),calc(var(--f-size-unit, 1px) * var(--f-size-min, 16) + (var(--f-size-max, 16) - var(--f-size-min, 16)) * (var(--f-size-container, 100vw) - (var(--f-size-unit, 1px) * var(--f-size-min-container, 320))) / (var(--f-size-max-container, 1920) - var(--f-size-min-container, 320))),calc(var(--f-size-unit, 1px) * var(--f-size-max, 16)));height:clamp(calc(var(--f-size-unit, 1px) * var(--f-size-min, 16)),calc(var(--f-size-unit, 1px) * var(--f-size-min, 16) + (var(--f-size-max, 16) - var(--f-size-min, 16)) * (var(--f-size-container, 100vw) - (var(--f-size-unit, 1px) * var(--f-size-min-container, 320))) / (var(--f-size-max-container, 1920) - var(--f-size-min-container, 320))),calc(var(--f-size-unit, 1px) * var(--f-size-max, 16)))}@property --un-gradient-fn-from{syntax: ""; inherits: false; initial-value: #000}@property --un-gradient-fn-to{syntax: ""; inherits: false; initial-value: #000}@property --un-gradient-fn-color-space{syntax: ""; inherits: false; initial-value: in oklch;}@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--un-text-opacity:100%;--un-bg-opacity:100%;--un-outline-opacity:100%;--un-outline-style:solid}}@property --un-text-opacity{syntax:"";inherits:false;initial-value:100%;}@property --un-leading{syntax:"*";inherits:false;}@property --un-outline-opacity{syntax:"";inherits:false;initial-value:100%;}@property --un-outline-style{syntax:"*";inherits:false;initial-value:solid;}@property --un-bg-opacity{syntax:"";inherits:false;initial-value:100%;}@property --un-inset-ring-color{syntax:"*";inherits:false;}@property --un-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-inset-shadow-color{syntax:"*";inherits:false;}@property --un-ring-color{syntax:"*";inherits:false;}@property --un-ring-inset{syntax:"*";inherits:false;}@property --un-ring-offset-color{syntax:"*";inherits:false;}@property --un-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-ring-offset-width{syntax:"";inherits:false;initial-value:0px;}@property --un-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-shadow-color{syntax:"*";inherits:false;}@property --un-ease{syntax:"*";inherits:false;}.i-nimiq\:cross,[i-nimiq\:cross=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m1 1 10 10m0-10L1 11'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}.i-nimiq\:exclamation{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg clip-path='url(%23IconifyId197c99756a03b6a5b0)'%3E%3Cpath fill='currentColor' d='M6.214 0C6.838 0 7.34.502 7.34 1.125v5.25c0 .623-.501 1.125-1.125 1.125A1.12 1.12 0 015.09 6.375v-5.25C5.09.502 5.591 0 6.214 0m-1.5 10.5a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId197c99756a03b6a5b0'%3E%3Cpath fill='%23fff' d='M0 0h12v12H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}.i-nimiq\:info,[i-nimiq\:info=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 13 13'%3E%3Cg fill='none'%3E%3Crect width='1.368' height='3.649' x='7.184' y='9.693' fill='currentColor' rx='.643' transform='rotate(-180 7.184 9.693)'/%3E%3Crect width='1.825' height='1.825' x='7.412' y='4.904' fill='currentColor' rx='.857' transform='rotate(-180 7.412 4.904)'/%3E%3Cpath stroke='currentColor' stroke-linecap='round' stroke-width='1.5' d='M6.5 12.202A5.7 5.7 0 116.5.8a5.7 5.7 0 010 11.402z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}.i-nimiq\:leaf-2-filled,[i-nimiq\:leaf-2-filled=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='currentColor' d='M12 2.462V4c0 .525-.1 1.045-.297 1.53a4 4 0 01-.845 1.298 3.9 3.9 0 01-1.266.868C9.12 7.896 8.612 8 8.1 8H6.6v3.077H5.4V6.769l.011-.615a4.04 4.04 0 011.24-2.628A3.85 3.85 0 019.3 2.462zM2.4 0c.882 0 1.74.285 2.456.813a4.3 4.3 0 011.53 2.132c-.46.4-.835.889-1.106 1.44a4.7 4.7 0 00-.47 1.769H4.2a4.15 4.15 0 01-2.97-1.262A4.36 4.36 0 010 1.846V0z'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}.i-nimiq\:mountain-frame{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg stroke='currentColor' stroke-width='.732' clip-path='url(%23IconifyId197c99756a03b6a5b2)'%3E%3Cpath d='M10.32.676H1.68A1.08 1.08 0 00.6 1.756v8.64c0 .597.484 1.08 1.08 1.08h8.64a1.08 1.08 0 001.08-1.08v-8.64a1.08 1.08 0 00-1.08-1.08Z'/%3E%3Cpath d='M8.31 4.823a.96.96 0 100-1.92.96.96 0 000 1.92ZM.72 8.97l2.973-2.974a.406.406 0 01.53-.04l3.114 2.308c.144.106.34.106.484 0L9.459 7.05a.41.41 0 01.484 0l1.577 1.17'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId197c99756a03b6a5b2'%3E%3Cpath fill='%23fff' d='M0 0h12v12H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}.i-nimiq\:spinner{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg stroke='currentColor' stroke-linecap='round' stroke-width='1.667' clip-path='url(%23IconifyId197c99756a03b6a5b1)'%3E%3Cpath d='M6 1C8.7625 1 11 3.2375 11 6'/%3E%3Cpath d='M3.03809 2C1.80402 2.90374 1 4.36219 1 6.01385C1 8.7687 3.2313 11 5.98615 11C7.63781 11 9.09626 10.196 10 8.96191' opacity='.3'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId197c99756a03b6a5b1'%3E%3Crect width='12' height='12' fill='%23fff'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3CanimateTransform attributeName='transform' dur='1s' from='0 0 0' repeatCount='indefinite' to='360 0 0' type='rotate'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}.i-nimiq\:star,[i-nimiq\:star=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='currentColor' d='M5.886 10.22a.23.23 0 01.227 0l2.875 1.618a1.18 1.18 0 001.385-.153 1.16 1.16 0 00.292-1.339L9.54 7.751a.23.23 0 01.058-.264L11.64 5.67a1.16 1.16 0 00.277-1.272 1.14 1.14 0 00-1.08-.706H8.53a.23.23 0 01-.213-.14L7.076.677 7.063.65a1.195 1.195 0 00-2.14.026L3.68 3.551a.23.23 0 01-.213.14H1.16a1.135 1.135 0 00-1.077.7 1.17 1.17 0 00.294 1.292l2.027 1.804a.23.23 0 01.058.264l-1.13 2.598a1.16 1.16 0 00.29 1.34 1.18 1.18 0 001.391.152z'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}[i-nimiq\:exclamation=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg clip-path='url(%23IconifyId197c99756a03b6a5b3)'%3E%3Cpath fill='currentColor' d='M6.214 0C6.838 0 7.34.502 7.34 1.125v5.25c0 .623-.501 1.125-1.125 1.125A1.12 1.12 0 015.09 6.375v-5.25C5.09.502 5.591 0 6.214 0m-1.5 10.5a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId197c99756a03b6a5b3'%3E%3Cpath fill='%23fff' d='M0 0h12v12H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}[i-nimiq\:mountain-frame=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg stroke='currentColor' stroke-width='.732' clip-path='url(%23IconifyId197c99756a03b6a5b5)'%3E%3Cpath d='M10.32.676H1.68A1.08 1.08 0 00.6 1.756v8.64c0 .597.484 1.08 1.08 1.08h8.64a1.08 1.08 0 001.08-1.08v-8.64a1.08 1.08 0 00-1.08-1.08Z'/%3E%3Cpath d='M8.31 4.823a.96.96 0 100-1.92.96.96 0 000 1.92ZM.72 8.97l2.973-2.974a.406.406 0 01.53-.04l3.114 2.308c.144.106.34.106.484 0L9.459 7.05a.41.41 0 01.484 0l1.577 1.17'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId197c99756a03b6a5b5'%3E%3Cpath fill='%23fff' d='M0 0h12v12H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}[i-nimiq\:spinner=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg stroke='currentColor' stroke-linecap='round' stroke-width='1.667' clip-path='url(%23IconifyId197c99756a03b6a5b4)'%3E%3Cpath d='M6 1C8.7625 1 11 3.2375 11 6'/%3E%3Cpath d='M3.03809 2C1.80402 2.90374 1 4.36219 1 6.01385C1 8.7687 3.2313 11 5.98615 11C7.63781 11 9.09626 10.196 10 8.96191' opacity='.3'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId197c99756a03b6a5b4'%3E%3Crect width='12' height='12' fill='%23fff'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3CanimateTransform attributeName='transform' dur='1s' from='0 0 0' repeatCount='indefinite' to='360 0 0' type='rotate'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}:root,:host{--spacing: .0625rem;--font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-serif: ui-serif,Georgia,Cambria,"Times New Roman",Times,serif;--font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--colors-white: light-dark(oklch(1 0 90), oklch(1 0 90));--colors-darkblue: light-dark(oklch(.2737 .068 276.29), oklch(.2737 .068 276.29));--colors-darkerblue: light-dark(oklch(.2182 .0371 280.55), oklch(.2182 .0371 280.55));--colors-neutral-0: light-dark(oklch(1 0 90), oklch(.2737 .068 276.29));--colors-neutral-50: light-dark(oklch(.9881 0 89.88), oklch(.2388 .0344 281));--colors-neutral-100: light-dark(oklch(.9791 0 89.88), oklch(.2472 .0341 281.14));--colors-neutral-200: light-dark(oklch(.9677 .0027 286.35), oklch(.2679 .0334 281.42));--colors-neutral-300: light-dark(oklch(.95 .004 286.32), oklch(.2842 .033 281.61));--colors-neutral-400: light-dark(oklch(.9203 .0067 286.27), oklch(.3117 .0304 281.85));--colors-neutral-500: light-dark(oklch(.8681 .0096 279.67), oklch(.3991 .0252 282.18));--colors-neutral-600: light-dark(oklch(.8347 .0125 281.04), oklch(.4429 .0229 282.19));--colors-neutral-700: light-dark(oklch(.6613 .0281 280.83), oklch(.64 .0148 285.97));--colors-neutral-800: light-dark(oklch(.5889 .0335 281.21), oklch(.7168 .0101 279.62));--colors-neutral-900: light-dark(oklch(.4374 .0495 279.71), oklch(.8619 .0055 286.28));--colors-neutral-DEFAULT: light-dark(oklch(.2737 .068 276.29), oklch(1 0 90));--colors-neutral-gradient-from: light-dark(oklab(.2737 .0075 -.0676), oklab(.1553 .0542 -.0519));--colors-neutral-gradient-to: light-dark(oklab(.2018 .0685 -.0675), oklab(.2221 .0059 -.0521));--colors-neutral-gradient-darkened-from: light-dark(, );--colors-neutral-gradient-darkened-to: light-dark(, );--colors-blue-400: light-dark(oklch(.9545 .0167 236.69), oklch(.2716 .0521 257.92));--colors-blue-500: light-dark(oklch(.9109 .0327 232.24), oklch(.3193 .0701 251.54));--colors-blue-600: light-dark(oklch(.8885 .0428 231.75), oklch(.3477 .0771 249.07));--colors-blue-1100: light-dark(oklch(.481 .1164 243.72), oklch(.6982 .1694 243.83));--colors-blue-DEFAULT: light-dark(oklch(.5849 .1438 244.29), oklch(.6982 .1694 243.83));--colors-blue-gradient-from: light-dark(oklab(.5849 -.0624 -.1296), oklab(.6982 -.0747 -.152));--colors-blue-gradient-to: light-dark(oklab(.5849 -.0624 -.1296), oklab(.6982 -.0747 -.152));--colors-blue-gradient-darkened-from: light-dark(oklab(.4857 -.022 -.1807), oklab(.5755 -.048 -.1764));--colors-blue-gradient-darkened-to: light-dark(oklab(.5404 -.0523 -.1438), oklab(.6546 -.0695 -.1422));--colors-green-400: light-dark(oklch(.9637 .017 187.9), oklch(.2764 .0331 242.34));--colors-green-500: light-dark(oklch(.9307 .034 185.2), oklch(.333 .0416 210.14));--colors-green-600: light-dark(oklch(.9154 .0432 185.62), oklch(.3622 .0475 203.62));--colors-green-1100: light-dark(oklch(.5564 .0992 178.59), oklch(.755 .1426 170.23));--colors-green-DEFAULT: light-dark(oklch(.6932 .1245 178.48), oklch(.755 .1426 170.23));--colors-green-gradient-to: light-dark(oklab(.6932 -.1245 .0033), oklab(.755 -.1405 .0242));--colors-green-gradient-from: light-dark(oklab(.6 -.1198 .0072), oklab(.65 -.1659 .0372));--colors-green-gradient-darkened-from: light-dark(oklab(.6231 -.0909 .0017), oklab(.7142 -.1422 .0319));--colors-green-gradient-darkened-to: light-dark(oklab(.6873 -.1184 -.0011), oklab(.6971 -.1282 .0212));--colors-orange-400: light-dark(oklch(.951 .0221 74.1), oklch(.2755 .014 3.07));--colors-orange-500: light-dark(oklch(.9396 .0436 71.7), oklch(.3344 .0381 61.07));--colors-orange-600: light-dark(oklch(.9251 .0549 71.49), oklch(.3634 .0513 63.86));--colors-orange-1100: light-dark(oklch(.6769 .1633 57), oklch(.772 .1738 64.55));--colors-orange-DEFAULT: light-dark(oklch(.7387 .179 56.67), oklch(.772 .1738 64.55));--colors-orange-gradient-from: light-dark(oklab(.65 .1657 .1447), oklab(.7 .1105 .1667));--colors-orange-gradient-to: light-dark(oklab(.7387 .0984 .1496), oklab(.772 .0747 .1569));--colors-orange-gradient-darkened-from: light-dark(oklab(.6387 .153 .1288), oklab(.7442 .1001 .1509));--colors-orange-gradient-darkened-to: light-dark(oklab(.7115 .1244 .144), oklab(.7438 .0703 .1509));--colors-red-400: light-dark(oklch(.9544 .0166 26.65), oklch(.2655 .0357 328.64));--colors-red-500: light-dark(oklch(.9112 .0328 27.11), oklch(.3103 .0508 358.44));--colors-red-600: light-dark(oklch(.8878 .0422 25.25), oklch(.3368 .0603 6.45));--colors-red-1100: light-dark(oklch(.515 .1713 30.54), oklch(.6881 .2018 30.03));--colors-red-DEFAULT: light-dark(oklch(.598 .1886 30.3), oklch(.6881 .2018 30.03));--colors-red-gradient-from: light-dark(oklab(.5 .2088 .0692), oklab(.6 .2142 .1288));--colors-red-gradient-to: light-dark(oklab(.598 .1628 .0952), oklab(.6881 .1747 .101));--colors-red-gradient-darkened-from: light-dark(oklab(.5344 .1734 .0523), oklab(.6515 .2016 .1213));--colors-red-gradient-darkened-to: light-dark(oklab(.5653 .177 .0874), oklab(.6633 .1597 .0937));--colors-gold-400: light-dark(oklch(.9765 .022 89.79), oklch(.2916 .009 340.92));--colors-gold-500: light-dark(oklch(.9556 .0434 91.27), oklch(.3564 .0252 69.32));--colors-gold-600: light-dark(oklch(.9434 .0539 92.15), oklch(.3918 .0369 76.19));--colors-gold-1100: light-dark(oklch(.6642 .1329 85.55), oklch(.8517 .1579 83.77));--colors-gold-DEFAULT: light-dark(oklch(.7924 .1593 85.61), oklch(.8517 .1579 83.77));--colors-gold-gradient-from: light-dark(oklab(.7 .0595 .1592), oklab(.78 .0503 .1624));--colors-gold-gradient-to: light-dark(oklab(.7924 .0122 .1588), oklab(.8517 .0171 .157));--colors-gold-gradient-darkened-from: light-dark(oklab(.7143 .0695 .139), oklab(.8163 .0458 .1479));--colors-gold-gradient-darkened-to: light-dark(oklab(.7575 .0315 .1515), oklab(.8221 .0167 .1498));--colors-purple-400: light-dark(oklch(.9494 .0083 301.35), oklch(.2524 .0591 291.66));--colors-purple-500: light-dark(oklch(.8984 .0181 300.04), oklch(.2869 .0801 296.38));--colors-purple-600: light-dark(oklch(.8725 .0224 300.16), oklch(.3037 .0894 298.08));--colors-purple-1100: light-dark(oklch(.3841 .0825 296.42), oklch(.5483 .2189 304.41));--colors-purple-DEFAULT: light-dark(oklch(.4629 .1027 296.59), oklch(.5483 .2189 304.41));--text-xs-fontSize: .1875rem;--text-xs-lineHeight: .25rem;--text-sm-fontSize: .21875rem;--text-sm-lineHeight: .3125rem;--text-base-fontSize: .25rem;--text-base-lineHeight: .375rem;--text-lg-fontSize: .28125rem;--text-lg-lineHeight: .4375rem;--text-xl-fontSize: .3125rem;--text-xl-lineHeight: .4375rem;--text-2xl-fontSize: .375rem;--text-2xl-lineHeight: .5rem;--text-3xl-fontSize: .46875rem;--text-3xl-lineHeight: .5625rem;--text-4xl-fontSize: .5625rem;--text-4xl-lineHeight: .625rem;--text-5xl-fontSize: .75rem;--text-5xl-lineHeight: 1;--text-6xl-fontSize: .9375rem;--text-6xl-lineHeight: 1;--text-7xl-fontSize: 1.125rem;--text-7xl-lineHeight: 1;--text-8xl-fontSize: 1.5rem;--text-8xl-lineHeight: 1;--text-9xl-fontSize: 2rem;--text-9xl-lineHeight: 1;--fontWeight-thin: 100;--fontWeight-extralight: 200;--fontWeight-light: 300;--fontWeight-normal: 400;--fontWeight-medium: 500;--fontWeight-semibold: 600;--fontWeight-bold: 700;--fontWeight-extrabold: 800;--fontWeight-black: 900;--tracking-tighter: -.05em;--tracking-tight: -.025em;--tracking-normal: 0em;--tracking-wide: .025em;--tracking-wider: .05em;--tracking-widest: .1em;--leading-none: 1;--leading-tight: 1.25;--leading-snug: 1.375;--leading-normal: 1.5;--leading-relaxed: 1.625;--leading-loose: 2;--textStrokeWidth-DEFAULT: .375rem;--textStrokeWidth-none: 0;--textStrokeWidth-sm: thin;--textStrokeWidth-md: medium;--textStrokeWidth-lg: thick;--radius-DEFAULT: .0625rem;--radius-none: 0;--radius-xs: .03125rem;--radius-sm: .0625rem;--radius-md: .09375rem;--radius-lg: .125rem;--radius-xl: .1875rem;--radius-2xl: .25rem;--radius-3xl: .375rem;--radius-4xl: .5rem;--ease-DEFAULT: var(--nq-ease);--blur-DEFAULT: 8px;--blur-xs: 4px;--blur-sm: 8px;--blur-md: 12px;--blur-lg: 16px;--blur-xl: 24px;--blur-2xl: 40px;--blur-3xl: 64px;--perspective-dramatic: 100px;--perspective-near: 300px;--perspective-normal: 500px;--perspective-midrange: 800px;--perspective-distant: 1200px;--default-transition-duration: .15s;--default-transition-timingFunction: cubic-bezier(.4, 0, .2, 1);--default-font-family: var(--font-sans);--default-font-featureSettings: var(--font-sans--font-feature-settings);--default-font-variationSettings: var(--font-sans--font-variation-settings);--default-monoFont-family: var(--font-mono);--default-monoFont-featureSettings: var(--font-mono--font-feature-settings);--default-monoFont-variationSettings: var(--font-mono--font-variation-settings);--container-3xs: 4rem;--container-2xs: 4.5rem;--container-xs: 5rem;--container-sm: 6rem;--container-md: 7rem;--container-lg: 8rem;--container-xl: 9rem;--container-2xl: 10.5rem;--container-3xl: 12rem;--container-4xl: 14rem;--container-5xl: 16rem;--container-6xl: 18rem;--container-7xl: 20rem;--container-prose: 65ch;--transitionProperty-colors: color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-fn-from,--un-gradient-fn-to}:root{--colors-white: oklch(1 0 90);--colors-darkblue: oklch(.2737 .068 276.29);--colors-darkerblue: oklch(.2182 .0371 280.55);--colors-neutral-0: light-dark(oklch(1 0 90), oklch(.2737 .068 276.29));--colors-neutral-50: light-dark(oklch(.9881 0 89.88), oklch(.2388 .0344 281));--colors-neutral-100: light-dark(oklch(.9791 0 89.88), oklch(.2472 .0341 281.14));--colors-neutral-200: light-dark(oklch(.9677 .0027 286.35), oklch(.2679 .0334 281.42));--colors-neutral-300: light-dark(oklch(.95 .004 286.32), oklch(.2842 .033 281.61));--colors-neutral-400: light-dark(oklch(.9203 .0067 286.27), oklch(.3117 .0304 281.85));--colors-neutral-500: light-dark(oklch(.8681 .0096 279.67), oklch(.3991 .0252 282.18));--colors-neutral-600: light-dark(oklch(.8347 .0125 281.04), oklch(.4429 .0229 282.19));--colors-neutral-700: light-dark(oklch(.6613 .0281 280.83), oklch(.64 .0148 285.97));--colors-neutral-800: light-dark(oklch(.5889 .0335 281.21), oklch(.7168 .0101 279.62));--colors-neutral-900: light-dark(oklch(.4374 .0495 279.71), oklch(.8619 .0055 286.28));--colors-neutral: light-dark(oklch(.2737 .068 276.29), oklch(1 0 90));--colors-blue-400: light-dark(oklch(.9545 .0167 236.69), oklch(.2716 .0521 257.92));--colors-blue-500: light-dark(oklch(.9109 .0327 232.24), oklch(.3193 .0701 251.54));--colors-blue-600: light-dark(oklch(.8885 .0428 231.75), oklch(.3477 .0771 249.07));--colors-blue-1100: light-dark(oklch(.481 .1164 243.72), oklch(.6982 .1694 243.83));--colors-blue: light-dark(oklch(.5849 .1438 244.29), oklch(.6982 .1694 243.83));--colors-green-400: light-dark(oklch(.9637 .017 187.9), oklch(.2764 .0331 242.34));--colors-green-500: light-dark(oklch(.9307 .034 185.2), oklch(.333 .0416 210.14));--colors-green-600: light-dark(oklch(.9154 .0432 185.62), oklch(.3622 .0475 203.62));--colors-green-1100: light-dark(oklch(.5564 .0992 178.59), oklch(.755 .1426 170.23));--colors-green: light-dark(oklch(.6932 .1245 178.48), oklch(.755 .1426 170.23));--colors-orange-400: light-dark(oklch(.951 .0221 74.1), oklch(.2755 .014 3.07));--colors-orange-500: light-dark(oklch(.9396 .0436 71.7), oklch(.3344 .0381 61.07));--colors-orange-600: light-dark(oklch(.9251 .0549 71.49), oklch(.3634 .0513 63.86));--colors-orange-1100: light-dark(oklch(.6769 .1633 57), oklch(.772 .1738 64.55));--colors-orange: light-dark(oklch(.7387 .179 56.67), oklch(.772 .1738 64.55));--colors-red-400: light-dark(oklch(.9544 .0166 26.65), oklch(.2655 .0357 328.64));--colors-red-500: light-dark(oklch(.9112 .0328 27.11), oklch(.3103 .0508 358.44));--colors-red-600: light-dark(oklch(.8878 .0422 25.25), oklch(.3368 .0603 6.45));--colors-red-1100: light-dark(oklch(.515 .1713 30.54), oklch(.6881 .2018 30.03));--colors-red: light-dark(oklch(.598 .1886 30.3), oklch(.6881 .2018 30.03));--colors-gold-400: light-dark(oklch(.9765 .022 89.79), oklch(.2916 .009 340.92));--colors-gold-500: light-dark(oklch(.9556 .0434 91.27), oklch(.3564 .0252 69.32));--colors-gold-600: light-dark(oklch(.9434 .0539 92.15), oklch(.3918 .0369 76.19));--colors-gold-1100: light-dark(oklch(.6642 .1329 85.55), oklch(.8517 .1579 83.77));--colors-gold: light-dark(oklch(.7924 .1593 85.61), oklch(.8517 .1579 83.77));--colors-purple-400: light-dark(oklch(.9494 .0083 301.35), oklch(.2524 .0591 291.66));--colors-purple-500: light-dark(oklch(.8984 .0181 300.04), oklch(.2869 .0801 296.38));--colors-purple-600: light-dark(oklch(.8725 .0224 300.16), oklch(.3037 .0894 298.08));--colors-purple-1100: light-dark(oklch(.3841 .0825 296.42), oklch(.5483 .2189 304.41));--colors-purple: light-dark(oklch(.4629 .1027 296.59), oklch(.5483 .2189 304.41));--colors-neutral-gradient-from: light-dark(oklab(.2737 .0075 -.0676), oklab(.1553 .0542 -.0519));--colors-neutral-gradient-to: light-dark(oklab(.2018 .0685 -.0675), oklab(.2221 .0059 -.0521));--colors-neutral-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-neutral-gradient-from), var(--colors-neutral-gradient-to));--colors-blue-gradient-from: light-dark(oklab(.5849 -.0624 -.1296), oklab(.6982 -.0747 -.152));--colors-blue-gradient-to: light-dark(oklab(.5849 -.0624 -.1296), oklab(.6982 -.0747 -.152));--colors-blue-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-blue-gradient-from), var(--colors-blue-gradient-to));--colors-blue-gradient-darkened-from: light-dark(oklab(.4857 -.022 -.1807), oklab(.5755 -.048 -.1764));--colors-blue-gradient-darkened-to: light-dark(oklab(.5404 -.0523 -.1438), oklab(.6546 -.0695 -.1422));--colors-blue-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-blue-gradient-darkened-from), var(--colors-blue-gradient-darkened-to));--colors-green-gradient-from: light-dark(oklab(.6 -.1198 .0072), oklab(.65 -.1659 .0372));--colors-green-gradient-to: light-dark(oklab(.6932 -.1245 .0033), oklab(.755 -.1405 .0242));--colors-green-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-green-gradient-from), var(--colors-green-gradient-to));--colors-green-gradient-darkened-from: light-dark(oklab(.6231 -.0909 .0017), oklab(.7142 -.1422 .0319));--colors-green-gradient-darkened-to: light-dark(oklab(.6873 -.1184 -.0011), oklab(.6971 -.1282 .0212));--colors-green-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-green-gradient-darkened-from), var(--colors-green-gradient-darkened-to));--colors-orange-gradient-from: light-dark(oklab(.65 .1657 .1447), oklab(.7 .1105 .1667));--colors-orange-gradient-to: light-dark(oklab(.7387 .0984 .1496), oklab(.772 .0747 .1569));--colors-orange-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-orange-gradient-from), var(--colors-orange-gradient-to));--colors-orange-gradient-darkened-from: light-dark(oklab(.6387 .153 .1288), oklab(.7442 .1001 .1509));--colors-orange-gradient-darkened-to: light-dark(oklab(.7115 .1244 .144), oklab(.7438 .0703 .1509));--colors-orange-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-orange-gradient-darkened-from), var(--colors-orange-gradient-darkened-to));--colors-red-gradient-from: light-dark(oklab(.5 .2088 .0692), oklab(.6 .2142 .1288));--colors-red-gradient-to: light-dark(oklab(.598 .1628 .0952), oklab(.6881 .1747 .101));--colors-red-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-red-gradient-from), var(--colors-red-gradient-to));--colors-red-gradient-darkened-from: light-dark(oklab(.5344 .1734 .0523), oklab(.6515 .2016 .1213));--colors-red-gradient-darkened-to: light-dark(oklab(.5653 .177 .0874), oklab(.6633 .1597 .0937));--colors-red-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-red-gradient-darkened-from), var(--colors-red-gradient-darkened-to));--colors-gold-gradient-from: light-dark(oklab(.7 .0595 .1592), oklab(.78 .0503 .1624));--colors-gold-gradient-to: light-dark(oklab(.7924 .0122 .1588), oklab(.8517 .0171 .157));--colors-gold-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-gold-gradient-from), var(--colors-gold-gradient-to));--colors-gold-gradient-darkened-from: light-dark(oklab(.7143 .0695 .139), oklab(.8163 .0458 .1479));--colors-gold-gradient-darkened-to: light-dark(oklab(.7575 .0315 .1515), oklab(.8221 .0167 .1498));--colors-gold-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-gold-gradient-darkened-from), var(--colors-gold-gradient-darkened-to))}.bg-gradient-gold{background-image:var(--colors-gold-gradient)}.bg-gradient-green{background-image:var(--colors-green-gradient)}.bg-gradient-red{background-image:var(--colors-red-gradient)}:root{--nq-ease: cubic-bezier(.25, 0, 0, 1);--nq-shadow: 0px 18px 38px rgba(31 35 72 / .07), 0px 7px 8.5px rgba(31 35 72 / .04), 0px 2px 2.5px rgba(31 35 72 / .02);--nq-shadow-lg: 0px 6px 20px rgba(59 76 106 / .13), 0px 1.34018px 4.46726px rgba(59 76 106 / .0774939), 0px .399006px 1.33002px rgba(59 76 106 / .0525061);--nq-track: var(--colors-neutral-200);--nq-thumb: var(--colors-neutral-600);--nq-font-sans: "Mulish", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;--nq-font-mono: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--nq-m-min: 0;--nq-m-max: 0;--nq-screen-width-min: 320;--nq-screen-width-max: 1152;--font-size-min: 16;--font-size-max: 16}:where(*:not(:where(.nq-raw *,[nq-raw] *))){--f-font-width-range: calc(var(--nq-screen-width-max) - var(--nq-screen-width-min));--f-font-scale-factor: calc((100vw - (1px * var(--nq-screen-width-min))) / var(--f-font-width-range));--font-size-range: calc(var(--font-size-max) - var(--font-size-min));--font-size-fluid: calc(1px * var(--font-size-min) + var(--font-size-range) * var(--f-font-scale-factor));--font-size: clamp(calc(1px * var(--font-size-min)), var(--font-size-fluid), calc(var(--font-size-max) * 1px));border-color:var(--colors-neutral-400);outline-color:var(--colors-blue);outline-width:1.5px;overscroll-behavior-x:contain}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(:not(:where(code,pre))){font-family:var(--nq-font-sans, "Mulish")}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(code,pre,pre *,code *){font-family:var(--nq-font-mono, "Fira Code")}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(*,*:before,*:after){box-sizing:inherit}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(body){box-sizing:border-box;-webkit-overflow-scrolling:touch;min-width:300px;min-height:100vh;min-height:100dvh;color:var(--colors-neutral-900);background-color:var(--colors-neutral-0)}:where([data-inverted] *) :where(*:not(:where(.nq-raw *,[nq-raw] *))):where(body){color:color-mix(in oklch,var(--colors-neutral-0) 80%)}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(h1,h2,h3,h4,h5,h6){text-wrap:balance;width:100%;max-width:100%;font-weight:700;font-size:var(--font-size);line-height:1.2;color:var(--colors-neutral)}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(h1){--font-size-min: 24;--font-size-max: 32}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(h2){--font-size-max: 20;--font-size-min: 24}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(p){--line-height: 1.3125}:where(*:not(:where(.nq-raw *,[nq-raw] *))) small{--font-size-min: 12;--font-size-max: 14;font-size:var(--font-size);line-height:1.2;font-weight:600}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(p,li){text-wrap:pretty}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(p,span):empty{display:none}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(a){cursor:pointer}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(blockquote p,blockquote p>:where(em,b)){color:var(--colors-green)}:where(*:not(:where(.nq-raw *,[nq-raw] *))):not(:where(.shiki *)):where(code,pre){color:var(--colors-neutral-900);--font-size-min: 14;--font-size-max: 16;font-size:var(--font-size);overflow-x:auto;--nq-track: var(--colors-neutral-500);--nq-thumb: color-mix(in oklch, var(--colors-neutral-700) 75%, transparent)}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(code){line-height:1.875rem}:where(*:not(:where(.nq-raw *,[nq-raw] *))) :where(:where(h1,h2,h3,h4,h5,h6,p,li,span,a)>code){background-color:var(--colors-neutral-200)}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(pre){background-color:var(--colors-neutral-50)}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(a,button:not([disabled])){position:relative}@media not all and (hover: hover){:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(a,button:not([disabled])):before{content:"";position:absolute;left:-1rem;top:-1rem;right:-1rem;bottom:-1rem}}:where(*:not(:where(.nq-raw *,[nq-raw] *))):where(input[type=text],textarea){caret-color:var(--colors-blue)}:root{color-scheme:light dark}.f-text-sm,[f-text-sm=""]{--f-text-min:14;--f-text-max:16;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)))}.f-size-md{--f-size-min:24;--f-size-max:32;width:clamp(calc(var(--f-size-unit, 1px) * var(--f-size-min, 16)),calc(var(--f-size-unit, 1px) * var(--f-size-min, 16) + (var(--f-size-max, 16) - var(--f-size-min, 16)) * (var(--f-size-container, 100vw) - (var(--f-size-unit, 1px) * var(--f-size-min-container, 320))) / (var(--f-size-max-container, 1920) - var(--f-size-min-container, 320))),calc(var(--f-size-unit, 1px) * var(--f-size-max, 16)));height:clamp(calc(var(--f-size-unit, 1px) * var(--f-size-min, 16)),calc(var(--f-size-unit, 1px) * var(--f-size-min, 16) + (var(--f-size-max, 16) - var(--f-size-min, 16)) * (var(--f-size-container, 100vw) - (var(--f-size-unit, 1px) * var(--f-size-min-container, 320))) / (var(--f-size-max-container, 1920) - var(--f-size-min-container, 320))),calc(var(--f-size-unit, 1px) * var(--f-size-max, 16)))}.f-px-xs,[f-px-xs=""]{--f-px-min:12;--f-px-max:16;padding-left:clamp(calc(var(--f-px-unit, 1px) * var(--f-px-min, 16)),calc(var(--f-px-unit, 1px) * var(--f-px-min, 16) + (var(--f-px-max, 16) - var(--f-px-min, 16)) * (var(--f-px-container, 100vw) - (var(--f-px-unit, 1px) * var(--f-px-min-container, 320))) / (var(--f-px-max-container, 1920) - var(--f-px-min-container, 320))),calc(var(--f-px-unit, 1px) * var(--f-px-max, 16)));padding-right:clamp(calc(var(--f-px-unit, 1px) * var(--f-px-min, 16)),calc(var(--f-px-unit, 1px) * var(--f-px-min, 16) + (var(--f-px-max, 16) - var(--f-px-min, 16)) * (var(--f-px-container, 100vw) - (var(--f-px-unit, 1px) * var(--f-px-min-container, 320))) / (var(--f-px-max-container, 1920) - var(--f-px-min-container, 320))),calc(var(--f-px-unit, 1px) * var(--f-px-max, 16)))}.f-mt-lg,[f-mt-lg=""]{--f-mt-min:32;--f-mt-max:48;margin-top:clamp(calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16)),calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16) + (var(--f-mt-max, 16) - var(--f-mt-min, 16)) * (var(--f-mt-container, 100vw) - (var(--f-mt-unit, 1px) * var(--f-mt-min-container, 320))) / (var(--f-mt-max-container, 1920) - var(--f-mt-min-container, 320))),calc(var(--f-mt-unit, 1px) * var(--f-mt-max, 16)))}.f-mt-md,[f-mt-md=""]{--f-mt-min:24;--f-mt-max:32;margin-top:clamp(calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16)),calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16) + (var(--f-mt-max, 16) - var(--f-mt-min, 16)) * (var(--f-mt-container, 100vw) - (var(--f-mt-unit, 1px) * var(--f-mt-min-container, 320))) / (var(--f-mt-max-container, 1920) - var(--f-mt-min-container, 320))),calc(var(--f-mt-unit, 1px) * var(--f-mt-max, 16)))}.f-mt-sm,[f-mt-sm=""]{--f-mt-min:16;--f-mt-max:24;margin-top:clamp(calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16)),calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16) + (var(--f-mt-max, 16) - var(--f-mt-min, 16)) * (var(--f-mt-container, 100vw) - (var(--f-mt-unit, 1px) * var(--f-mt-min-container, 320))) / (var(--f-mt-max-container, 1920) - var(--f-mt-min-container, 320))),calc(var(--f-mt-unit, 1px) * var(--f-mt-max, 16)))}.f-mb-md,[f-mb-md=""]{--f-mb-min:24;--f-mb-max:32;margin-bottom:clamp(calc(var(--f-mb-unit, 1px) * var(--f-mb-min, 16)),calc(var(--f-mb-unit, 1px) * var(--f-mb-min, 16) + (var(--f-mb-max, 16) - var(--f-mb-min, 16)) * (var(--f-mb-container, 100vw) - (var(--f-mb-unit, 1px) * var(--f-mb-min-container, 320))) / (var(--f-mb-max-container, 1920) - var(--f-mb-min-container, 320))),calc(var(--f-mb-unit, 1px) * var(--f-mb-max, 16)))}.f-p-2xs,[f-p-2xs=""]{--f-p-min:8;--f-p-max:12;padding:clamp(calc(var(--f-p-unit, 1px) * var(--f-p-min, 16)),calc(var(--f-p-unit, 1px) * var(--f-p-min, 16) + (var(--f-p-max, 16) - var(--f-p-min, 16)) * (var(--f-p-container, 100vw) - (var(--f-p-unit, 1px) * var(--f-p-min-container, 320))) / (var(--f-p-max-container, 1920) - var(--f-p-min-container, 320))),calc(var(--f-p-unit, 1px) * var(--f-p-max, 16)))}.f-p-md{--f-p-min:24;--f-p-max:32;padding:clamp(calc(var(--f-p-unit, 1px) * var(--f-p-min, 16)),calc(var(--f-p-unit, 1px) * var(--f-p-min, 16) + (var(--f-p-max, 16) - var(--f-p-min, 16)) * (var(--f-p-container, 100vw) - (var(--f-p-unit, 1px) * var(--f-p-min-container, 320))) / (var(--f-p-max-container, 1920) - var(--f-p-min-container, 320))),calc(var(--f-p-unit, 1px) * var(--f-p-max, 16)))}.f-p-sm,[f-p-sm=""]{--f-p-min:16;--f-p-max:24;padding:clamp(calc(var(--f-p-unit, 1px) * var(--f-p-min, 16)),calc(var(--f-p-unit, 1px) * var(--f-p-min, 16) + (var(--f-p-max, 16) - var(--f-p-min, 16)) * (var(--f-p-container, 100vw) - (var(--f-p-unit, 1px) * var(--f-p-min-container, 320))) / (var(--f-p-max-container, 1920) - var(--f-p-min-container, 320))),calc(var(--f-p-unit, 1px) * var(--f-p-max, 16)))}.f-rounded-md{--f-rounded-min:6;--f-rounded-max:8;border-radius:clamp(calc(var(--f-rounded-unit, 1px) * var(--f-rounded-min, 16)),calc(var(--f-rounded-unit, 1px) * var(--f-rounded-min, 16) + (var(--f-rounded-max, 16) - var(--f-rounded-min, 16)) * (var(--f-rounded-container, 100vw) - (var(--f-rounded-unit, 1px) * var(--f-rounded-min-container, 320))) / (var(--f-rounded-max-container, 1920) - var(--f-rounded-min-container, 320))),calc(var(--f-rounded-unit, 1px) * var(--f-rounded-max, 16)))}:where(.stack>*),:where([stack=""]>*){grid-area:1 / 1;justify-self:center;align-self:center}.stack,[stack=""]{width:100%;display:grid;place-content:center;grid-template-columns:1fr;grid-template-rows:1fr}@property --nq-gradient-from{syntax: ""; inherits: false; initial-value: #0000;}@property --nq-gradient-to{syntax: ""; inherits: false; initial-value: #0000;}@property --nq-gradient-stops{syntax: "*"; inherits: false;}@keyframes mask-up{to{-webkit-mask-size:100% var(--mask-size),100% 100%,100% var(--mask-size);mask-size:100% var(--mask-size),100% 100%,100% var(--mask-size)}}@keyframes mask-down{to{-webkit-mask-size:100% var(--mask-size),100% 100%,100% 0;mask-size:100% var(--mask-size),100% 100%,100% 0}}.nq-label{text-transform:uppercase;letter-spacing:.17em;font-size:.75em;font-weight:700;line-height:1;color:var(--colors-neutral-800)}:where([data-inverted] *) .nq-label{color:color-mix(in oklch,var(--colors-neutral-0) 80%,transparent)}:where(.dark,[data-theme=dark]) .nq-label{color:color-mix(in oklch,var(--colors-white) 70%,transparent)}[nq-label=""]{text-transform:uppercase;letter-spacing:.17em;font-size:.75em;font-weight:700;line-height:1;color:var(--colors-neutral-800)}:where([data-inverted] *) [nq-label=""]{color:color-mix(in oklch,var(--colors-neutral-0) 80%,transparent)}:where(.dark,[data-theme=dark]) [nq-label=""]{color:color-mix(in oklch,var(--colors-white) 70%,transparent)}.nq-pill-xl{position:relative;font-weight:700;font-size:.875rem;padding:.1875rem .8125rem;border:none;border-radius:999px;display:flex;align-items:center;justify-content:center;gap:.125rem;line-height:1.5;height:max-content;width:max-content;text-decoration:none;color:#fff;outline:none;background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);--transitions: --nq-gradient-from .2s var(--nq-ease), --nq-gradient-to .2s var(--nq-ease);transition:var(--transitions)}.nq-pill-xl:where(.nq-pill-lg,[nq-pill-lg]){font-size:1rem;padding:.3125rem 1rem}.nq-pill-xl:where(.nq-pill-xl,[nq-pill-xl]){box-shadow:0 .5rem 1.5rem #00000026;cursor:pointer;transition:transform .45s var(--nq-ease),box-shadow .45s var(--nq-ease),background-color .25s var(--nq-ease),var(--transitions);will-change:box-shadow;text-transform:uppercase;letter-spacing:.094em;min-width:12.5rem;flex:1;padding:1.25rem 2rem;line-height:1.25rem;font-size:1rem;gap:.5rem;border-radius:9999px;margin:1rem}.nq-pill-xl:where(.nq-pill-xl,[nq-pill-xl]):where(:hover,:focus){box-shadow:0 1rem 2.5rem #0003;transform:translate3D(0,-2px,0)}.nq-pill-xl:active{outline:none;box-shadow:0 .2rem .3rem #0003;transform:translate3D(0,1px,0);transition:transform .2s cubic-bezier(.41,.34,.26,1.55),box-shadow .2s cubic-bezier(.41,.34,.26,1.55),background-color .2s var(--nq-ease),var(--transitions)}.nq-pill-xl:where([disabled]){pointer-events:none;cursor:not-allowed;opacity:.5}.nq-pill-xl:not([disabled]):where(:hover,:active,:focus):before{opacity:1}.nq-pill-xl:focus-visible{outline:2px solid var(--colors-blue);outline-offset:3px}[nq-pill-xl=""]{position:relative;font-weight:700;font-size:.875rem;padding:.1875rem .8125rem;border:none;border-radius:999px;display:flex;align-items:center;justify-content:center;gap:.125rem;line-height:1.5;height:max-content;width:max-content;text-decoration:none;color:#fff;outline:none;background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);--transitions: --nq-gradient-from .2s var(--nq-ease), --nq-gradient-to .2s var(--nq-ease);transition:var(--transitions)}[nq-pill-xl=""]:where(.nq-pill-lg,[nq-pill-lg]){font-size:1rem;padding:.3125rem 1rem}[nq-pill-xl=""]:where(.nq-pill-xl,[nq-pill-xl]){box-shadow:0 .5rem 1.5rem #00000026;cursor:pointer;transition:transform .45s var(--nq-ease),box-shadow .45s var(--nq-ease),background-color .25s var(--nq-ease),var(--transitions);will-change:box-shadow;text-transform:uppercase;letter-spacing:.094em;min-width:12.5rem;flex:1;padding:1.25rem 2rem;line-height:1.25rem;font-size:1rem;gap:.5rem;border-radius:9999px;margin:1rem}[nq-pill-xl=""]:where(.nq-pill-xl,[nq-pill-xl]):where(:hover,:focus){box-shadow:0 1rem 2.5rem #0003;transform:translate3D(0,-2px,0)}[nq-pill-xl=""]:active{outline:none;box-shadow:0 .2rem .3rem #0003;transform:translate3D(0,1px,0);transition:transform .2s cubic-bezier(.41,.34,.26,1.55),box-shadow .2s cubic-bezier(.41,.34,.26,1.55),background-color .2s var(--nq-ease),var(--transitions)}[nq-pill-xl=""]:where([disabled]){pointer-events:none;cursor:not-allowed;opacity:.5}[nq-pill-xl=""]:not([disabled]):where(:hover,:active,:focus):before{opacity:1}[nq-pill-xl=""]:focus-visible{outline:2px solid var(--colors-blue);outline-offset:3px}.nq-pill-blue{position:relative;font-weight:700;font-size:.875rem;padding:.1875rem .8125rem;border:none;border-radius:999px;display:flex;align-items:center;justify-content:center;gap:.125rem;line-height:1.5;height:max-content;width:max-content;text-decoration:none;color:#fff;outline:none;background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);--transitions: --nq-gradient-from .2s var(--nq-ease), --nq-gradient-to .2s var(--nq-ease);transition:var(--transitions);--nq-gradient-from: var(--colors-blue-gradient-from);--nq-gradient-to: var(--colors-blue-gradient-to)}.nq-pill-blue:where(.nq-pill-lg,[nq-pill-lg]){font-size:1rem;padding:.3125rem 1rem}.nq-pill-blue:where(.nq-pill-xl,[nq-pill-xl]){box-shadow:0 .5rem 1.5rem #00000026;cursor:pointer;transition:transform .45s var(--nq-ease),box-shadow .45s var(--nq-ease),background-color .25s var(--nq-ease),var(--transitions);will-change:box-shadow;text-transform:uppercase;letter-spacing:.094em;min-width:12.5rem;flex:1;padding:1.25rem 2rem;line-height:1.25rem;font-size:1rem;gap:.5rem;border-radius:9999px;margin:1rem}.nq-pill-blue:where(.nq-pill-xl,[nq-pill-xl]):where(:hover,:focus){box-shadow:0 1rem 2.5rem #0003;transform:translate3D(0,-2px,0)}.nq-pill-blue:active{outline:none;box-shadow:0 .2rem .3rem #0003;transform:translate3D(0,1px,0);transition:transform .2s cubic-bezier(.41,.34,.26,1.55),box-shadow .2s cubic-bezier(.41,.34,.26,1.55),background-color .2s var(--nq-ease),var(--transitions)}.nq-pill-blue:where([disabled]){pointer-events:none;cursor:not-allowed;opacity:.5}.nq-pill-blue:not([disabled]):where(:hover,:active,:focus):before{opacity:1}.nq-pill-blue:focus-visible{outline:2px solid var(--colors-blue);outline-offset:3px}.nq-pill-blue:hover,.nq-pill-blue:focus-visible{--nq-gradient-from: var(--colors-blue-gradient-darkened-from);--nq-gradient-to: var(--colors-blue-gradient-darkened-to)}[nq-pill-blue=""]{position:relative;font-weight:700;font-size:.875rem;padding:.1875rem .8125rem;border:none;border-radius:999px;display:flex;align-items:center;justify-content:center;gap:.125rem;line-height:1.5;height:max-content;width:max-content;text-decoration:none;color:#fff;outline:none;background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);--transitions: --nq-gradient-from .2s var(--nq-ease), --nq-gradient-to .2s var(--nq-ease);transition:var(--transitions);--nq-gradient-from: var(--colors-blue-gradient-from);--nq-gradient-to: var(--colors-blue-gradient-to)}[nq-pill-blue=""]:where(.nq-pill-lg,[nq-pill-lg]){font-size:1rem;padding:.3125rem 1rem}[nq-pill-blue=""]:where(.nq-pill-xl,[nq-pill-xl]){box-shadow:0 .5rem 1.5rem #00000026;cursor:pointer;transition:transform .45s var(--nq-ease),box-shadow .45s var(--nq-ease),background-color .25s var(--nq-ease),var(--transitions);will-change:box-shadow;text-transform:uppercase;letter-spacing:.094em;min-width:12.5rem;flex:1;padding:1.25rem 2rem;line-height:1.25rem;font-size:1rem;gap:.5rem;border-radius:9999px;margin:1rem}[nq-pill-blue=""]:where(.nq-pill-xl,[nq-pill-xl]):where(:hover,:focus){box-shadow:0 1rem 2.5rem #0003;transform:translate3D(0,-2px,0)}[nq-pill-blue=""]:active{outline:none;box-shadow:0 .2rem .3rem #0003;transform:translate3D(0,1px,0);transition:transform .2s cubic-bezier(.41,.34,.26,1.55),box-shadow .2s cubic-bezier(.41,.34,.26,1.55),background-color .2s var(--nq-ease),var(--transitions)}[nq-pill-blue=""]:where([disabled]){pointer-events:none;cursor:not-allowed;opacity:.5}[nq-pill-blue=""]:not([disabled]):where(:hover,:active,:focus):before{opacity:1}[nq-pill-blue=""]:focus-visible{outline:2px solid var(--colors-blue);outline-offset:3px}[nq-pill-blue=""]:hover,[nq-pill-blue=""]:focus-visible{--nq-gradient-from: var(--colors-blue-gradient-darkened-from);--nq-gradient-to: var(--colors-blue-gradient-darkened-to)}.nq-switch:where(input[type=checkbox]){--active-color: var(--colors-neutral-300);-webkit-appearance:none;-moz-appearance:none;appearance:none;font-size:1em;width:2em;height:max-content;aspect-ratio:1.625;border-radius:2em;background:var(--active-color);position:relative;cursor:pointer}.nq-switch:where(input[type=checkbox]):before{content:"";position:absolute;transform:translate(-50%,-50%);top:50%;left:.6em;width:.9em;height:.9em;border:1px solid color-mix(in oklch,var(--colors-neutral) 2%,transparent);border-radius:50%;background:#fff;transition:left .1s ease-out;box-shadow:var(--nq-shadow)}.nq-switch:where(input[type=checkbox]):checked{--active-color: var(--colors-blue)}.nq-switch:where(input[type=checkbox]):checked:before{left:1.4em}@media (prefers-reduced-motion){.nq-switch:where(input[type=checkbox]){transition-duration:0s}.nq-switch:where(input[type=checkbox]):before{transition-duration:0s}}.nq-switch:where(input[type=checkbox])[disabled]{opacity:.6;cursor:not-allowed}[nq-switch=""]:where(input[type=checkbox]){--active-color: var(--colors-neutral-300);-webkit-appearance:none;-moz-appearance:none;appearance:none;font-size:1em;width:2em;height:max-content;aspect-ratio:1.625;border-radius:2em;background:var(--active-color);position:relative;cursor:pointer}[nq-switch=""]:where(input[type=checkbox]):before{content:"";position:absolute;transform:translate(-50%,-50%);top:50%;left:.6em;width:.9em;height:.9em;border:1px solid color-mix(in oklch,var(--colors-neutral) 2%,transparent);border-radius:50%;background:#fff;transition:left .1s ease-out;box-shadow:var(--nq-shadow)}[nq-switch=""]:where(input[type=checkbox]):checked{--active-color: var(--colors-blue)}[nq-switch=""]:where(input[type=checkbox]):checked:before{left:1.4em}@media (prefers-reduced-motion){[nq-switch=""]:where(input[type=checkbox]){transition-duration:0s}[nq-switch=""]:where(input[type=checkbox]):before{transition-duration:0s}}[nq-switch=""]:where(input[type=checkbox])[disabled]{opacity:.6;cursor:not-allowed}.nq-input-box{font-size:1em;line-height:1.5;padding:.28125em .875em;height:max-content;min-width:min(8em,220px);border-radius:.125em;width:100%;background-color:transparent;transition:colors .2s var(--nq-ease),box-shadow .1s var(--nq-ease);--color: var(--colors-neutral);--placeholder-color: var(--colors-neutral-700);--outline-color: var(--colors-neutral-500);outline:1.5px solid var(--outline-color);color:var(--color)}:where([data-inverted] *) .nq-input-box{--placeholder-color: color-mix(in oklch, var(--colors-neutral-0) 80%, transparent)}.nq-input-box::placeholder{color:var(--placeholder-color)}.nq-input-box:not(.nq-invalid,:user-invalid):where(:hover,:focus,:focus-within){--color: var(--colors-blue);--outline-color: color-mix(in oklch, var(--colors-blue) 30%, transparent)}.nq-input-box:not(.nq-invalid,:user-invalid):where(:hover,:focus,:focus-within)::placeholder{--placeholder-color: color-mix(in oklch, var(--colors-blue) 60%, transparent)}.nq-input-box:not(.nq-invalid,:user-invalid):where(:hover,:focus,:focus-within):focus:valid,.nq-input-box:not(.nq-invalid,:user-invalid):where(:hover,:focus,:focus-within):focus-within:valid{--outline-color: var(--colors-blue)}.nq-input-box:where(.nq-invalid,:user-invalid){--color: var(--colors-orange);outline-color:color-mix(in oklch,var(--colors-orange) 40%,transparent)}.nq-input-box:where(.nq-invalid,:user-invalid)::placeholder{--placeholder-color: color-mix(in oklch, var(--colors-orange) 70%, transparent)}.nq-input-box:where(.nq-invalid,:user-invalid):hover,.nq-input-box:where(.nq-invalid,:user-invalid):focus,.nq-input-box:where(.nq-invalid,:user-invalid):focus-within{outline-color:var(--colors-orange)}.nq-input-box:where(textarea,:has(textarea)){--padding: .25em;min-height:calc(3lh + 2 * var(--padding));max-height:calc(5lh + 2 * var(--padding));field-sizing:content;padding:var(--padding) calc(var(--padding) + ((1lh - 1ex) / 2))}[nq-input-box=""]{font-size:1em;line-height:1.5;padding:.28125em .875em;height:max-content;min-width:min(8em,220px);border-radius:.125em;width:100%;background-color:transparent;transition:colors .2s var(--nq-ease),box-shadow .1s var(--nq-ease);--color: var(--colors-neutral);--placeholder-color: var(--colors-neutral-700);--outline-color: var(--colors-neutral-500);outline:1.5px solid var(--outline-color);color:var(--color)}:where([data-inverted] *) [nq-input-box=""]{--placeholder-color: color-mix(in oklch, var(--colors-neutral-0) 80%, transparent)}[nq-input-box=""]::placeholder{color:var(--placeholder-color)}[nq-input-box=""]:not(.nq-invalid,:user-invalid):where(:hover,:focus,:focus-within){--color: var(--colors-blue);--outline-color: color-mix(in oklch, var(--colors-blue) 30%, transparent)}[nq-input-box=""]:not(.nq-invalid,:user-invalid):where(:hover,:focus,:focus-within)::placeholder{--placeholder-color: color-mix(in oklch, var(--colors-blue) 60%, transparent)}[nq-input-box=""]:not(.nq-invalid,:user-invalid):where(:hover,:focus,:focus-within):focus:valid,[nq-input-box=""]:not(.nq-invalid,:user-invalid):where(:hover,:focus,:focus-within):focus-within:valid{--outline-color: var(--colors-blue)}[nq-input-box=""]:where(.nq-invalid,:user-invalid){--color: var(--colors-orange);outline-color:color-mix(in oklch,var(--colors-orange) 40%,transparent)}[nq-input-box=""]:where(.nq-invalid,:user-invalid)::placeholder{--placeholder-color: color-mix(in oklch, var(--colors-orange) 70%, transparent)}[nq-input-box=""]:where(.nq-invalid,:user-invalid):hover,[nq-input-box=""]:where(.nq-invalid,:user-invalid):focus,[nq-input-box=""]:where(.nq-invalid,:user-invalid):focus-within{outline-color:var(--colors-orange)}[nq-input-box=""]:where(textarea,:has(textarea)){--padding: .25em;min-height:calc(3lh + 2 * var(--padding));max-height:calc(5lh + 2 * var(--padding));field-sizing:content;padding:var(--padding) calc(var(--padding) + ((1lh - 1ex) / 2))}.nq-hoverable-green{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-green-gradient-from);--nq-gradient-to: var(--colors-green-gradient-to)}@media (hover: hover){.nq-hoverable-green:where(:hover,:focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--colors-darkblue);box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}.nq-hoverable-green>*{z-index:1}}.nq-hoverable-green:hover,.nq-hoverable-green:focus-visible{--nq-gradient-from: var(--colors-green-gradient-darkened-from);--nq-gradient-to: var(--colors-green-gradient-darkened-to)}[nq-hoverable-green=""]{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-green-gradient-from);--nq-gradient-to: var(--colors-green-gradient-to)}@media (hover: hover){[nq-hoverable-green=""]:where(:hover,:focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--colors-darkblue);box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}[nq-hoverable-green=""]>*{z-index:1}}[nq-hoverable-green=""]:hover,[nq-hoverable-green=""]:focus-visible{--nq-gradient-from: var(--colors-green-gradient-darkened-from);--nq-gradient-to: var(--colors-green-gradient-darkened-to)}.nq-hoverable-red{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-red-gradient-from);--nq-gradient-to: var(--colors-red-gradient-to)}@media (hover: hover){.nq-hoverable-red:where(:hover,:focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--colors-darkblue);box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}.nq-hoverable-red>*{z-index:1}}.nq-hoverable-red:hover,.nq-hoverable-red:focus-visible{--nq-gradient-from: var(--colors-red-gradient-darkened-from);--nq-gradient-to: var(--colors-red-gradient-darkened-to)}[nq-hoverable-red=""]{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-red-gradient-from);--nq-gradient-to: var(--colors-red-gradient-to)}@media (hover: hover){[nq-hoverable-red=""]:where(:hover,:focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--colors-darkblue);box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}[nq-hoverable-red=""]>*{z-index:1}}[nq-hoverable-red=""]:hover,[nq-hoverable-red=""]:focus-visible{--nq-gradient-from: var(--colors-red-gradient-darkened-from);--nq-gradient-to: var(--colors-red-gradient-darkened-to)}.nq-hoverable-gold{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-gold-gradient-from);--nq-gradient-to: var(--colors-gold-gradient-to)}@media (hover: hover){.nq-hoverable-gold:where(:hover,:focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--colors-darkblue);box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}.nq-hoverable-gold>*{z-index:1}}.nq-hoverable-gold:hover,.nq-hoverable-gold:focus-visible{--nq-gradient-from: var(--colors-gold-gradient-darkened-from);--nq-gradient-to: var(--colors-gold-gradient-darkened-to)}[nq-hoverable-gold=""]{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-gold-gradient-from);--nq-gradient-to: var(--colors-gold-gradient-to)}@media (hover: hover){[nq-hoverable-gold=""]:where(:hover,:focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--colors-darkblue);box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}[nq-hoverable-gold=""]>*{z-index:1}}[nq-hoverable-gold=""]:hover,[nq-hoverable-gold=""]:focus-visible{--nq-gradient-from: var(--colors-gold-gradient-darkened-from);--nq-gradient-to: var(--colors-gold-gradient-darkened-to)}.text-14,[text-14=""]{font-size:.875rem}[text~="10"]{font-size:.625rem}[text~="12"]{font-size:.75rem}[text~="24"]{font-size:1.5rem}:where(.group,[group]):hover [text~="group-hocus:neutral-800"],:where(.group,[group]):focus-visible [text~="group-hocus:neutral-800"],.text-neutral-800,[text-neutral-800=""],[text~=neutral-800]{color:color-mix(in srgb,var(--colors-neutral-800) var(--un-text-opacity),transparent)}.text-neutral-700,[text-neutral-700=""],[text~=neutral-700]{color:color-mix(in srgb,var(--colors-neutral-700) var(--un-text-opacity),transparent)}.text-white,[text-white=""]{color:color-mix(in srgb,var(--colors-white) var(--un-text-opacity),transparent)}[text~="[&:has(~_label:hover)]:gold"]:has(~label:hover),[text~="data-[state=active]:gold"][data-state=active],[text~="hocus:gold"]:hover,[text~="hocus:gold"]:focus-visible{color:color-mix(in srgb,var(--colors-gold-DEFAULT) var(--un-text-opacity),transparent)}[text~=neutral-300]{color:color-mix(in srgb,var(--colors-neutral-300) var(--un-text-opacity),transparent)}[text~=neutral]{color:color-mix(in srgb,var(--colors-neutral-DEFAULT) var(--un-text-opacity),transparent)}[text~=red-1100]{color:color-mix(in srgb,var(--colors-red-1100) var(--un-text-opacity),transparent)}[un-text-current=""]{color:currentColor}.lh-24{--un-leading:calc(var(--spacing) * 24);line-height:calc(var(--spacing) * 24)}.lh-none,[lh-none=""]{--un-leading:var(--leading-none);line-height:var(--leading-none)}.font-bold,[font-bold=""]{--un-font-weight:var(--fontWeight-bold);font-weight:var(--fontWeight-bold)}.font-mono,[font-mono=""]{font-family:var(--font-mono)}.font-normal,[font-normal=""]{--un-font-weight:var(--fontWeight-normal);font-weight:var(--fontWeight-normal)}.font-semibold,[font-semibold=""]{--un-font-weight:var(--fontWeight-semibold);font-weight:var(--fontWeight-semibold)}.mx-0,[mx-0=""]{margin-inline:calc(var(--spacing) * 0)}.mx-auto,[mx-auto=""]{margin-inline:auto}.mb-0,[mb-0=""]{margin-bottom:calc(var(--spacing) * 0)}.mb-12,[mb-12=""]{margin-bottom:calc(var(--spacing) * 12)}.mb-16,[mb-16=""]{margin-bottom:calc(var(--spacing) * 16)}.mb-8,[mb-8=""]{margin-bottom:calc(var(--spacing) * 8)}.mt--8,[mt--8=""]{margin-top:calc(var(--spacing) * -8)}.mt-2,[mt-2=""]{margin-top:calc(var(--spacing) * 2)}.mt-6,[mt-6=""]{margin-top:calc(var(--spacing) * 6)}.mt-auto,[mt-auto=""]{margin-top:auto}.px-1\.5,[px-1\.5=""]{padding-inline:calc(var(--spacing) * 1.5)}.px-2,[px-2=""]{padding-inline:calc(var(--spacing) * 2)}[text~=center]{text-align:center}.outline-1\.5,[outline~="[&:has(:focus-visible)]:1.5"]:has(:focus-visible),[outline~="1.5"]{outline-style:var(--un-outline-style);outline-width:1.5px}.outline-neutral-200{outline-color:color-mix(in srgb,var(--colors-neutral-200) var(--un-outline-opacity),transparent)}[outline~=blue]{outline-color:color-mix(in srgb,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}[outline~="neutral/15"]{outline-color:color-mix(in srgb,var(--colors-neutral-DEFAULT) 15%,transparent)}[outline~=red-500]{outline-color:color-mix(in srgb,var(--colors-red-500) var(--un-outline-opacity),transparent)}[outline~="white/8"]{outline-color:color-mix(in srgb,var(--colors-white) 8%,transparent)}.focus-visible\:outline-blue:focus-visible{outline-color:color-mix(in srgb,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}[focus-visible\:outline-blue=""]:focus-visible{outline-color:color-mix(in srgb,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}[outline~="offset--1.5"]{outline-offset:-1.5px}.outline{outline-style:var(--un-outline-style);outline-width:1px}.list-disc,[list-disc=""]{list-style-type:disc}.border{border-width:1px}.border-transparent{border-color:transparent}[border-transparent~="!"]{border-color:transparent!important}.rounded-3,[rounded-3=""]{border-radius:.1875rem}.rounded-4,[rounded-4=""]{border-radius:.25rem}.rounded-6,[rounded-6=""]{border-radius:.375rem}.rounded-full,[rounded-full=""]{border-radius:calc(infinity * 1px)}.border-none{--un-border-style:none;border-style:none}.bg-neutral-100,[bg-neutral-100=""]{background-color:color-mix(in srgb,var(--colors-neutral-100) var(--un-bg-opacity),transparent)}.bg-red-400,[bg-red-400=""]{background-color:color-mix(in srgb,var(--colors-red-400) var(--un-bg-opacity),transparent)}.bg-white,[bg-white=""]{background-color:color-mix(in srgb,var(--colors-white) var(--un-bg-opacity),transparent)}.op-0{opacity:0%}.op-100{opacity:100%}.op-90,[op-90=""]{opacity:90%}.disabled\:op-60:disabled{opacity:60%}[disabled\:op-60=""]:disabled{opacity:60%}.underline,[underline=""]{text-decoration-line:underline}.flex,[flex=""],[flex~="~"]{display:flex}.shrink-0,[shrink-0=""]{flex-shrink:0}.flex-col,[flex~=col]{flex-direction:column}.gap-16,[flex~=gap-16],[grid~=gap-16]{gap:calc(var(--spacing) * 16)}.gap-24,[flex~=gap-24]{gap:calc(var(--spacing) * 24)}.gap-8,[flex~=gap-8]{gap:calc(var(--spacing) * 8)}.grid,[grid=""],[grid~="~"]{display:grid}.col-span-2,[col-span-2=""]{grid-column:span 2/span 2}.cols-\[repeat\(auto-fit\,128px\)\],[grid~="cols-[repeat(auto-fit,128px)]"]{grid-template-columns:repeat(auto-fit,128px)}.cols-2,[grid~=cols-2]{grid-template-columns:repeat(2,minmax(0,1fr))}.rows-2,[grid~=rows-2]{grid-template-rows:repeat(2,minmax(0,1fr))}[rows~="4"]{grid-template-rows:repeat(4,minmax(0,1fr))}.size-128,[size-128=""]{width:calc(var(--spacing) * 128);height:calc(var(--spacing) * 128)}.size-128\!{width:calc(var(--spacing) * 128)!important;height:calc(var(--spacing) * 128)!important}.size-24,[size-24=""]{width:calc(var(--spacing) * 24);height:calc(var(--spacing) * 24)}.size-40,[size-40=""],[active~=size-40]:active{width:calc(var(--spacing) * 40);height:calc(var(--spacing) * 40)}.h-full,[h-full=""]{height:100%}.h-max,[h-max=""]{height:max-content}.h2{height:calc(var(--spacing) * 2)}.h3{height:calc(var(--spacing) * 3)}.max-h-128,[max-h-128=""]{max-height:calc(var(--spacing) * 128)}.w-auto,[w-auto=""]{width:auto}.w-full,[w-full=""]{width:100%}.w-max,[w-max=""]{width:max-content}[h-full~="!"]{height:100%!important}.aspect-square,[aspect-square=""]{aspect-ratio:1/1}.hidden,[hidden=""]{display:none}.cursor-pointer,[cursor-pointer=""],[active~=cursor-pointer]:active{cursor:pointer}.select-none,[select-none=""]{-webkit-user-select:none;user-select:none}.text-balance,[text-balance=""]{text-wrap:balance}.uppercase{text-transform:uppercase}.shadow,[shadow=""]{--un-shadow:var(--nq-shadow);box-shadow:var(--un-inset-shadow),var(--un-inset-ring-shadow),var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration))}.transition-colors,[transition-colors=""]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration))}[active~=transition-colors]:active{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration))}.duration-200{--un-duration:.2s;transition-duration:.2s}:where(.group,[group]):hover [delay~="group-hocus:[calc(var(--b)*(5-var(--i)))]"],:where(.group,[group]):focus-visible [delay~="group-hocus:[calc(var(--b)*(5-var(--i)))]"]{transition-delay:calc(var(--b) * (5 - var(--i)))}[delay~="[calc(25ms*var(--i))]"]{transition-delay:calc(25ms * var(--i))}.items-center,[flex~=items-center]{align-items:center}.self-start,[self-start=""]{align-self:flex-start}.justify-center,[flex~=justify-center]{justify-content:center}.right--12,[right--12=""]{right:calc(var(--spacing) * -12)}.top--12,[top--12=""]{top:calc(var(--spacing) * -12)}.justify-self-end,[justify-self-end=""]{justify-self:end}.sr-only,[sr-only=""]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.object-contain,[object-contain=""]{object-fit:contain}[text~=f-sm]{--f-text-min:14;--f-text-max:16;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)))}[text~=f-xs]{--f-text-min:12;--f-text-max:14;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)))}@supports (color: color-mix(in lab,red,red)){:where(.group,[group]):hover [text~="group-hocus:neutral-800"],:where(.group,[group]):focus-visible [text~="group-hocus:neutral-800"]{color:color-mix(in oklab,var(--colors-neutral-800) var(--un-text-opacity),transparent)}.text-neutral-700{color:color-mix(in oklab,var(--colors-neutral-700) var(--un-text-opacity),transparent)}.text-neutral-800{color:color-mix(in oklab,var(--colors-neutral-800) var(--un-text-opacity),transparent)}.text-white{color:color-mix(in oklab,var(--colors-white) var(--un-text-opacity),transparent)}[text-neutral-700=""]{color:color-mix(in oklab,var(--colors-neutral-700) var(--un-text-opacity),transparent)}[text-neutral-800=""]{color:color-mix(in oklab,var(--colors-neutral-800) var(--un-text-opacity),transparent)}[text-white=""]{color:color-mix(in oklab,var(--colors-white) var(--un-text-opacity),transparent)}[text~="[&:has(~_label:hover)]:gold"]:has(~label:hover){color:color-mix(in oklab,var(--colors-gold-DEFAULT) var(--un-text-opacity),transparent)}[text~="data-[state=active]:gold"][data-state=active]{color:color-mix(in oklab,var(--colors-gold-DEFAULT) var(--un-text-opacity),transparent)}[text~="hocus:gold"]:hover,[text~="hocus:gold"]:focus-visible{color:color-mix(in oklab,var(--colors-gold-DEFAULT) var(--un-text-opacity),transparent)}[text~=neutral-300]{color:color-mix(in oklab,var(--colors-neutral-300) var(--un-text-opacity),transparent)}[text~=neutral-700]{color:color-mix(in oklab,var(--colors-neutral-700) var(--un-text-opacity),transparent)}[text~=neutral-800]{color:color-mix(in oklab,var(--colors-neutral-800) var(--un-text-opacity),transparent)}[text~=neutral]{color:color-mix(in oklab,var(--colors-neutral-DEFAULT) var(--un-text-opacity),transparent)}[text~=red-1100]{color:color-mix(in oklab,var(--colors-red-1100) var(--un-text-opacity),transparent)}.outline-neutral-200{outline-color:color-mix(in oklab,var(--colors-neutral-200) var(--un-outline-opacity),transparent)}[outline~=blue]{outline-color:color-mix(in oklab,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}[outline~="neutral/15"]{outline-color:color-mix(in oklab,var(--colors-neutral-DEFAULT) 15%,transparent)}[outline~=red-500]{outline-color:color-mix(in oklab,var(--colors-red-500) var(--un-outline-opacity),transparent)}[outline~="white/8"]{outline-color:color-mix(in oklab,var(--colors-white) 8%,transparent)}.focus-visible\:outline-blue:focus-visible{outline-color:color-mix(in oklab,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}[focus-visible\:outline-blue=""]:focus-visible{outline-color:color-mix(in oklab,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}.bg-neutral-100{background-color:color-mix(in oklab,var(--colors-neutral-100) var(--un-bg-opacity),transparent)}.bg-red-400{background-color:color-mix(in oklab,var(--colors-red-400) var(--un-bg-opacity),transparent)}.bg-white{background-color:color-mix(in oklab,var(--colors-white) var(--un-bg-opacity),transparent)}[bg-neutral-100=""]{background-color:color-mix(in oklab,var(--colors-neutral-100) var(--un-bg-opacity),transparent)}[bg-red-400=""]{background-color:color-mix(in oklab,var(--colors-red-400) var(--un-bg-opacity),transparent)}[bg-white=""]{background-color:color-mix(in oklab,var(--colors-white) var(--un-bg-opacity),transparent)}} +#feedback-widget [data-app=nimiq-pay] [data-input=share-debug-info]{display:inherit!important}#feedback-widget [nq-input-box]{max-height:calc(20lh + 2 * var(--padding));border-radius:6px;padding:10px 12px;border:none;--border-color: var(--colors-neutral-400);outline:1.5px solid var(--border-color)}#feedback-widget [nq-input-box]:placeholder{--placeholder-color: var(--colors-neutral-500);color:var(--placeholder-color);transition:color .2s var(--nq-ease)}#feedback-widget [nq-input-box]:hover{--border-color: var(--colors-blue-600)}#feedback-widget [nq-input-box]:focus,#feedback-widget [nq-input-box]:focus-visible{--border-color: var(--colors-blue);color:var(--colors-blue);outline-style:solid;outline-width:1.5px}#feedback-widget [nq-pill-xl]{border-radius:9999px;color:var(--colors-white);width:100%;padding:1.5lh;line-height:1;font-weight:600;text-transform:uppercase;letter-spacing:.1em;font-size:15px}#feedback-widget .grid-container #feedback-widget button{@property --un-text-opacity{syntax:"";inherits:false;initial-value:100%;}}#feedback-widget .grid-container #feedback-widget button[data-v-f92a7dce]{color:color-mix(in srgb,var(--colors-white) var(--un-text-opacity),transparent);--un-border-style:none;border-style:none;display:flex;flex-direction:column;gap:calc(var(--spacing) * 8);cursor:pointer;align-items:center;justify-content:center;--f-text-min:14;--f-text-max:16;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)));--f-p-min:24;--f-p-max:32;padding:clamp(calc(var(--f-p-unit, 1px) * var(--f-p-min, 16)),calc(var(--f-p-unit, 1px) * var(--f-p-min, 16) + (var(--f-p-max, 16) - var(--f-p-min, 16)) * (var(--f-p-container, 100vw) - (var(--f-p-unit, 1px) * var(--f-p-min-container, 320))) / (var(--f-p-max-container, 1920) - var(--f-p-min-container, 320))),calc(var(--f-p-unit, 1px) * var(--f-p-max, 16)));--f-rounded-min:6;--f-rounded-max:8;border-radius:clamp(calc(var(--f-rounded-unit, 1px) * var(--f-rounded-min, 16)),calc(var(--f-rounded-unit, 1px) * var(--f-rounded-min, 16) + (var(--f-rounded-max, 16) - var(--f-rounded-min, 16)) * (var(--f-rounded-container, 100vw) - (var(--f-rounded-unit, 1px) * var(--f-rounded-min-container, 320))) / (var(--f-rounded-max-container, 1920) - var(--f-rounded-min-container, 320))),calc(var(--f-rounded-unit, 1px) * var(--f-rounded-max, 16)))}@supports (color: color-mix(in lab,red,red)){#feedback-widget .grid-container #feedback-widget button[data-v-f92a7dce]{color:color-mix(in oklab,var(--colors-white) var(--un-text-opacity),transparent)}}#feedback-widget .grid-container #feedback-widget button #feedback-widget>div[data-v-f92a7dce]:first-child{--f-size-min:24;--f-size-max:32;width:clamp(calc(var(--f-size-unit, 1px) * var(--f-size-min, 16)),calc(var(--f-size-unit, 1px) * var(--f-size-min, 16) + (var(--f-size-max, 16) - var(--f-size-min, 16)) * (var(--f-size-container, 100vw) - (var(--f-size-unit, 1px) * var(--f-size-min-container, 320))) / (var(--f-size-max-container, 1920) - var(--f-size-min-container, 320))),calc(var(--f-size-unit, 1px) * var(--f-size-max, 16)));height:clamp(calc(var(--f-size-unit, 1px) * var(--f-size-min, 16)),calc(var(--f-size-unit, 1px) * var(--f-size-min, 16) + (var(--f-size-max, 16) - var(--f-size-min, 16)) * (var(--f-size-container, 100vw) - (var(--f-size-unit, 1px) * var(--f-size-min-container, 320))) / (var(--f-size-max-container, 1920) - var(--f-size-min-container, 320))),calc(var(--f-size-unit, 1px) * var(--f-size-max, 16)))}@property --un-gradient-fn-from{syntax: ""; inherits: false; initial-value: #000}@property --un-gradient-fn-to{syntax: ""; inherits: false; initial-value: #000}@property --un-gradient-fn-color-space{syntax: ""; inherits: false; initial-value: in oklch;}@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){#feedback-widget *,#feedback-widget :before,#feedback-widget :after,#feedback-widget ::backdrop{--un-text-opacity:100%;--un-leading:initial;--un-ease:initial;--un-bg-opacity:100%;--un-outline-opacity:100%;--un-outline-style:solid}}@property --un-text-opacity{syntax:"";inherits:false;initial-value:100%;}@property --un-leading{syntax:"*";inherits:false;}@property --un-outline-opacity{syntax:"";inherits:false;initial-value:100%;}@property --un-outline-style{syntax:"*";inherits:false;initial-value:solid;}@property --un-bg-opacity{syntax:"";inherits:false;initial-value:100%;}@property --un-inset-ring-color{syntax:"*";inherits:false;}@property --un-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-inset-shadow-color{syntax:"*";inherits:false;}@property --un-ring-color{syntax:"*";inherits:false;}@property --un-ring-inset{syntax:"*";inherits:false;}@property --un-ring-offset-color{syntax:"*";inherits:false;}@property --un-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-ring-offset-width{syntax:"";inherits:false;initial-value:0px;}@property --un-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}@property --un-shadow-color{syntax:"*";inherits:false;}@property --un-ease{syntax:"*";inherits:false;}#feedback-widget .i-nimiq\:cross,#feedback-widget [i-nimiq\:cross=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m1 1 10 10m0-10L1 11'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget .i-nimiq\:exclamation{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg clip-path='url(%23IconifyId1997afcb597177a240)'%3E%3Cpath fill='currentColor' d='M6.214 0C6.838 0 7.34.502 7.34 1.125v5.25c0 .623-.501 1.125-1.125 1.125A1.12 1.12 0 015.09 6.375v-5.25C5.09.502 5.591 0 6.214 0m-1.5 10.5a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId1997afcb597177a240'%3E%3Cpath fill='%23fff' d='M0 0h12v12H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget .i-nimiq\:info,#feedback-widget [i-nimiq\:info=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 13 13'%3E%3Cg fill='none'%3E%3Crect width='1.368' height='3.649' x='7.184' y='9.693' fill='currentColor' rx='.643' transform='rotate(-180 7.184 9.693)'/%3E%3Crect width='1.825' height='1.825' x='7.412' y='4.904' fill='currentColor' rx='.857' transform='rotate(-180 7.412 4.904)'/%3E%3Cpath stroke='currentColor' stroke-linecap='round' stroke-width='1.5' d='M6.5 12.202A5.7 5.7 0 116.5.8a5.7 5.7 0 010 11.402z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget .i-nimiq\:leaf-2-filled,#feedback-widget [i-nimiq\:leaf-2-filled=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='currentColor' d='M12 2.462V4c0 .525-.1 1.045-.297 1.53a4 4 0 01-.845 1.298 3.9 3.9 0 01-1.266.868C9.12 7.896 8.612 8 8.1 8H6.6v3.077H5.4V6.769l.011-.615a4.04 4.04 0 011.24-2.628A3.85 3.85 0 019.3 2.462zM2.4 0c.882 0 1.74.285 2.456.813a4.3 4.3 0 011.53 2.132c-.46.4-.835.889-1.106 1.44a4.7 4.7 0 00-.47 1.769H4.2a4.15 4.15 0 01-2.97-1.262A4.36 4.36 0 010 1.846V0z'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget .i-nimiq\:mountain-frame{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg stroke='currentColor' stroke-width='.732' clip-path='url(%23IconifyId1997afcb597177a242)'%3E%3Cpath d='M10.32.676H1.68A1.08 1.08 0 00.6 1.756v8.64c0 .597.484 1.08 1.08 1.08h8.64a1.08 1.08 0 001.08-1.08v-8.64a1.08 1.08 0 00-1.08-1.08Z'/%3E%3Cpath d='M8.31 4.823a.96.96 0 100-1.92.96.96 0 000 1.92ZM.72 8.97l2.973-2.974a.406.406 0 01.53-.04l3.114 2.308c.144.106.34.106.484 0L9.459 7.05a.41.41 0 01.484 0l1.577 1.17'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId1997afcb597177a242'%3E%3Cpath fill='%23fff' d='M0 0h12v12H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget .i-nimiq\:spinner{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg stroke='currentColor' stroke-linecap='round' stroke-width='1.667' clip-path='url(%23IconifyId1997afcb597177a241)'%3E%3Cpath d='M6 1C8.7625 1 11 3.2375 11 6'/%3E%3Cpath d='M3.03809 2C1.80402 2.90374 1 4.36219 1 6.01385C1 8.7687 3.2313 11 5.98615 11C7.63781 11 9.09626 10.196 10 8.96191' opacity='.3'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId1997afcb597177a241'%3E%3Crect width='12' height='12' fill='%23fff'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3CanimateTransform attributeName='transform' dur='1s' from='0 0 0' repeatCount='indefinite' to='360 0 0' type='rotate'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget .i-nimiq\:star,#feedback-widget [i-nimiq\:star=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='currentColor' d='M5.886 10.22a.23.23 0 01.227 0l2.875 1.618a1.18 1.18 0 001.385-.153 1.16 1.16 0 00.292-1.339L9.54 7.751a.23.23 0 01.058-.264L11.64 5.67a1.16 1.16 0 00.277-1.272 1.14 1.14 0 00-1.08-.706H8.53a.23.23 0 01-.213-.14L7.076.677 7.063.65a1.195 1.195 0 00-2.14.026L3.68 3.551a.23.23 0 01-.213.14H1.16a1.135 1.135 0 00-1.077.7 1.17 1.17 0 00.294 1.292l2.027 1.804a.23.23 0 01.058.264l-1.13 2.598a1.16 1.16 0 00.29 1.34 1.18 1.18 0 001.391.152z'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget [i-nimiq\:exclamation=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg clip-path='url(%23IconifyId1997afcb597177a243)'%3E%3Cpath fill='currentColor' d='M6.214 0C6.838 0 7.34.502 7.34 1.125v5.25c0 .623-.501 1.125-1.125 1.125A1.12 1.12 0 015.09 6.375v-5.25C5.09.502 5.591 0 6.214 0m-1.5 10.5a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId1997afcb597177a243'%3E%3Cpath fill='%23fff' d='M0 0h12v12H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget [i-nimiq\:mountain-frame=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg stroke='currentColor' stroke-width='.732' clip-path='url(%23IconifyId1997afcb597177a245)'%3E%3Cpath d='M10.32.676H1.68A1.08 1.08 0 00.6 1.756v8.64c0 .597.484 1.08 1.08 1.08h8.64a1.08 1.08 0 001.08-1.08v-8.64a1.08 1.08 0 00-1.08-1.08Z'/%3E%3Cpath d='M8.31 4.823a.96.96 0 100-1.92.96.96 0 000 1.92ZM.72 8.97l2.973-2.974a.406.406 0 01.53-.04l3.114 2.308c.144.106.34.106.484 0L9.459 7.05a.41.41 0 01.484 0l1.577 1.17'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId1997afcb597177a245'%3E%3Cpath fill='%23fff' d='M0 0h12v12H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget [i-nimiq\:spinner=""]{--nq-icon:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cg fill='none'%3E%3Cg stroke='currentColor' stroke-linecap='round' stroke-width='1.667' clip-path='url(%23IconifyId1997afcb597177a244)'%3E%3Cpath d='M6 1C8.7625 1 11 3.2375 11 6'/%3E%3Cpath d='M3.03809 2C1.80402 2.90374 1 4.36219 1 6.01385C1 8.7687 3.2313 11 5.98615 11C7.63781 11 9.09626 10.196 10 8.96191' opacity='.3'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='IconifyId1997afcb597177a244'%3E%3Crect width='12' height='12' fill='%23fff'/%3E%3C/clipPath%3E%3C/defs%3E%3C/g%3E%3CanimateTransform attributeName='transform' dur='1s' from='0 0 0' repeatCount='indefinite' to='360 0 0' type='rotate'/%3E%3C/svg%3E");-webkit-mask:var(--nq-icon) no-repeat;mask:var(--nq-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;color:inherit}#feedback-widget :root,#feedback-widget{--spacing: .0625rem;--font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-serif: ui-serif,Georgia,Cambria,"Times New Roman",Times,serif;--font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--colors-white: light-dark(oklch(1 0 90), oklch(1 0 90));--colors-darkblue: light-dark(oklch(.2737 .068 276.29), oklch(.2737 .068 276.29));--colors-darkerblue: light-dark(oklch(.2182 .0371 280.55), oklch(.2182 .0371 280.55));--colors-neutral-0: light-dark(oklch(1 0 90), oklch(.2737 .068 276.29));--colors-neutral-50: light-dark(oklch(.9881 0 89.88), oklch(.2388 .0344 281));--colors-neutral-100: light-dark(oklch(.9791 0 89.88), oklch(.2472 .0341 281.14));--colors-neutral-200: light-dark(oklch(.9677 .0027 286.35), oklch(.2679 .0334 281.42));--colors-neutral-300: light-dark(oklch(.95 .004 286.32), oklch(.2842 .033 281.61));--colors-neutral-400: light-dark(oklch(.9203 .0067 286.27), oklch(.3117 .0304 281.85));--colors-neutral-500: light-dark(oklch(.8681 .0096 279.67), oklch(.3991 .0252 282.18));--colors-neutral-600: light-dark(oklch(.8347 .0125 281.04), oklch(.4429 .0229 282.19));--colors-neutral-700: light-dark(oklch(.6613 .0281 280.83), oklch(.64 .0148 285.97));--colors-neutral-800: light-dark(oklch(.5889 .0335 281.21), oklch(.7168 .0101 279.62));--colors-neutral-900: light-dark(oklch(.4374 .0495 279.71), oklch(.8619 .0055 286.28));--colors-neutral-1100: light-dark(oklch(1 0 90), oklch(.2185 .0213 246.2deg));--colors-neutral-DEFAULT: light-dark(oklch(.2737 .068 276.29), oklch(1 0 90));--colors-neutral-gradient-from: light-dark(oklab(.2737 .0075 -.0676), oklab(.1553 .0542 -.0519));--colors-neutral-gradient-to: light-dark(oklab(.2018 .0685 -.0675), oklab(.2221 .0059 -.0521));--colors-neutral-gradient-darkened-from: light-dark(, );--colors-neutral-gradient-darkened-to: light-dark(, );--colors-blue-400: light-dark(oklch(.9545 .0167 236.69), oklch(.2716 .0521 257.92));--colors-blue-500: light-dark(oklch(.9109 .0327 232.24), oklch(.3193 .0701 251.54));--colors-blue-600: light-dark(oklch(.8885 .0428 231.75), oklch(.3477 .0771 249.07));--colors-blue-1100: light-dark(oklch(.481 .1164 243.72), oklch(.6982 .1694 243.83));--colors-blue-DEFAULT: light-dark(oklch(.5849 .1438 244.29), oklch(.6982 .1694 243.83));--colors-blue-gradient-from: light-dark(oklab(.5849 -.0624 -.1296), oklab(.6982 -.0747 -.152));--colors-blue-gradient-to: light-dark(oklab(.5849 -.0624 -.1296), oklab(.6982 -.0747 -.152));--colors-blue-gradient-darkened-from: light-dark(oklab(.4857 -.022 -.1807), oklab(.5755 -.048 -.1764));--colors-blue-gradient-darkened-to: light-dark(oklab(.5404 -.0523 -.1438), oklab(.6546 -.0695 -.1422));--colors-green-400: light-dark(oklch(.9637 .017 187.9), oklch(.2764 .0331 242.34));--colors-green-500: light-dark(oklch(.9307 .034 185.2), oklch(.333 .0416 210.14));--colors-green-600: light-dark(oklch(.9154 .0432 185.62), oklch(.3622 .0475 203.62));--colors-green-1100: light-dark(oklch(.5564 .0992 178.59), oklch(.755 .1426 170.23));--colors-green-DEFAULT: light-dark(oklch(.6932 .1245 178.48), oklch(.755 .1426 170.23));--colors-green-gradient-to: light-dark(oklab(.6932 -.1245 .0033), oklab(.755 -.1405 .0242));--colors-green-gradient-from: light-dark(oklab(.6 -.1198 .0072), oklab(.65 -.1659 .0372));--colors-green-gradient-darkened-from: light-dark(oklab(.6231 -.0909 .0017), oklab(.7142 -.1422 .0319));--colors-green-gradient-darkened-to: light-dark(oklab(.6873 -.1184 -.0011), oklab(.6971 -.1282 .0212));--colors-orange-400: light-dark(oklch(.951 .0221 74.1), oklch(.2755 .014 3.07));--colors-orange-500: light-dark(oklch(.9396 .0436 71.7), oklch(.3344 .0381 61.07));--colors-orange-600: light-dark(oklch(.9251 .0549 71.49), oklch(.3634 .0513 63.86));--colors-orange-1100: light-dark(oklch(.6769 .1633 57), oklch(.772 .1738 64.55));--colors-orange-DEFAULT: light-dark(oklch(.7387 .179 56.67), oklch(.772 .1738 64.55));--colors-orange-gradient-from: light-dark(oklab(.65 .1657 .1447), oklab(.7 .1105 .1667));--colors-orange-gradient-to: light-dark(oklab(.7387 .0984 .1496), oklab(.772 .0747 .1569));--colors-orange-gradient-darkened-from: light-dark(oklab(.6387 .153 .1288), oklab(.7442 .1001 .1509));--colors-orange-gradient-darkened-to: light-dark(oklab(.7115 .1244 .144), oklab(.7438 .0703 .1509));--colors-red-400: light-dark(oklch(.9544 .0166 26.65), oklch(.2655 .0357 328.64));--colors-red-500: light-dark(oklch(.9112 .0328 27.11), oklch(.3103 .0508 358.44));--colors-red-600: light-dark(oklch(.8878 .0422 25.25), oklch(.3368 .0603 6.45));--colors-red-1100: light-dark(oklch(.515 .1713 30.54), oklch(.6881 .2018 30.03));--colors-red-DEFAULT: light-dark(oklch(.598 .1886 30.3), oklch(.6881 .2018 30.03));--colors-red-gradient-from: light-dark(oklab(.5 .2088 .0692), oklab(.6 .2142 .1288));--colors-red-gradient-to: light-dark(oklab(.598 .1628 .0952), oklab(.6881 .1747 .101));--colors-red-gradient-darkened-from: light-dark(oklab(.5344 .1734 .0523), oklab(.6515 .2016 .1213));--colors-red-gradient-darkened-to: light-dark(oklab(.5653 .177 .0874), oklab(.6633 .1597 .0937));--colors-gold-400: light-dark(oklch(.9765 .022 89.79), oklch(.2916 .009 340.92));--colors-gold-500: light-dark(oklch(.9556 .0434 91.27), oklch(.3564 .0252 69.32));--colors-gold-600: light-dark(oklch(.9434 .0539 92.15), oklch(.3918 .0369 76.19));--colors-gold-1100: light-dark(oklch(.6642 .1329 85.55), oklch(.8517 .1579 83.77));--colors-gold-DEFAULT: light-dark(oklch(.7924 .1593 85.61), oklch(.8517 .1579 83.77));--colors-gold-gradient-from: light-dark(oklab(.7 .0595 .1592), oklab(.78 .0503 .1624));--colors-gold-gradient-to: light-dark(oklab(.7924 .0122 .1588), oklab(.8517 .0171 .157));--colors-gold-gradient-darkened-from: light-dark(oklab(.7143 .0695 .139), oklab(.8163 .0458 .1479));--colors-gold-gradient-darkened-to: light-dark(oklab(.7575 .0315 .1515), oklab(.8221 .0167 .1498));--colors-purple-400: light-dark(oklch(.9494 .0083 301.35), oklch(.2524 .0591 291.66));--colors-purple-500: light-dark(oklch(.8984 .0181 300.04), oklch(.2869 .0801 296.38));--colors-purple-600: light-dark(oklch(.8725 .0224 300.16), oklch(.3037 .0894 298.08));--colors-purple-1100: light-dark(oklch(.3841 .0825 296.42), oklch(.5483 .2189 304.41));--colors-purple-DEFAULT: light-dark(oklch(.4629 .1027 296.59), oklch(.5483 .2189 304.41));--text-xs-fontSize: .1875rem;--text-xs-lineHeight: .25rem;--text-sm-fontSize: .21875rem;--text-sm-lineHeight: .3125rem;--text-base-fontSize: .25rem;--text-base-lineHeight: .375rem;--text-lg-fontSize: .28125rem;--text-lg-lineHeight: .4375rem;--text-xl-fontSize: .3125rem;--text-xl-lineHeight: .4375rem;--text-2xl-fontSize: .375rem;--text-2xl-lineHeight: .5rem;--text-3xl-fontSize: .46875rem;--text-3xl-lineHeight: .5625rem;--text-4xl-fontSize: .5625rem;--text-4xl-lineHeight: .625rem;--text-5xl-fontSize: .75rem;--text-5xl-lineHeight: 1;--text-6xl-fontSize: .9375rem;--text-6xl-lineHeight: 1;--text-7xl-fontSize: 1.125rem;--text-7xl-lineHeight: 1;--text-8xl-fontSize: 1.5rem;--text-8xl-lineHeight: 1;--text-9xl-fontSize: 2rem;--text-9xl-lineHeight: 1;--fontWeight-thin: 100;--fontWeight-extralight: 200;--fontWeight-light: 300;--fontWeight-normal: 400;--fontWeight-medium: 500;--fontWeight-semibold: 600;--fontWeight-bold: 700;--fontWeight-extrabold: 800;--fontWeight-black: 900;--tracking-tighter: -.05em;--tracking-tight: -.025em;--tracking-normal: 0em;--tracking-wide: .025em;--tracking-wider: .05em;--tracking-widest: .1em;--leading-none: 1;--leading-tight: 1.25;--leading-snug: 1.375;--leading-normal: 1.5;--leading-relaxed: 1.625;--leading-loose: 2;--textStrokeWidth-DEFAULT: .375rem;--textStrokeWidth-none: 0;--textStrokeWidth-sm: thin;--textStrokeWidth-md: medium;--textStrokeWidth-lg: thick;--radius-DEFAULT: .0625rem;--radius-none: 0;--radius-xs: .03125rem;--radius-sm: .0625rem;--radius-md: .09375rem;--radius-lg: .125rem;--radius-xl: .1875rem;--radius-2xl: .25rem;--radius-3xl: .375rem;--radius-4xl: .5rem;--ease-DEFAULT: var(--nq-ease);--blur-DEFAULT: 8px;--blur-xs: 4px;--blur-sm: 8px;--blur-md: 12px;--blur-lg: 16px;--blur-xl: 24px;--blur-2xl: 40px;--blur-3xl: 64px;--perspective-dramatic: 100px;--perspective-near: 300px;--perspective-normal: 500px;--perspective-midrange: 800px;--perspective-distant: 1200px;--default-transition-duration: .15s;--default-transition-timingFunction: cubic-bezier(.4, 0, .2, 1);--default-font-family: var(--font-sans);--default-font-featureSettings: var(--font-sans--font-feature-settings);--default-font-variationSettings: var(--font-sans--font-variation-settings);--default-monoFont-family: var(--font-mono);--default-monoFont-featureSettings: var(--font-mono--font-feature-settings);--default-monoFont-variationSettings: var(--font-mono--font-variation-settings);--container-3xs: 4rem;--container-2xs: 4.5rem;--container-xs: 5rem;--container-sm: 6rem;--container-md: 7rem;--container-lg: 8rem;--container-xl: 9rem;--container-2xl: 10.5rem;--container-3xl: 12rem;--container-4xl: 14rem;--container-5xl: 16rem;--container-6xl: 18rem;--container-7xl: 20rem;--container-prose: 65ch;--transitionProperty-colors: color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-fn-from,--un-gradient-fn-to}#feedback-widget{--colors-white: oklch(1 0 90);--colors-darkblue: oklch(.2737 .068 276.29);--colors-darkerblue: oklch(.2182 .0371 280.55);--colors-neutral-0: light-dark(oklch(1 0 90), oklch(.2737 .068 276.29));--colors-neutral-50: light-dark(oklch(.9881 0 89.88), oklch(.2388 .0344 281));--colors-neutral-100: light-dark(oklch(.9791 0 89.88), oklch(.2472 .0341 281.14));--colors-neutral-200: light-dark(oklch(.9677 .0027 286.35), oklch(.2679 .0334 281.42));--colors-neutral-300: light-dark(oklch(.95 .004 286.32), oklch(.2842 .033 281.61));--colors-neutral-400: light-dark(oklch(.9203 .0067 286.27), oklch(.3117 .0304 281.85));--colors-neutral-500: light-dark(oklch(.8681 .0096 279.67), oklch(.3991 .0252 282.18));--colors-neutral-600: light-dark(oklch(.8347 .0125 281.04), oklch(.4429 .0229 282.19));--colors-neutral-700: light-dark(oklch(.6613 .0281 280.83), oklch(.64 .0148 285.97));--colors-neutral-800: light-dark(oklch(.5889 .0335 281.21), oklch(.7168 .0101 279.62));--colors-neutral-900: light-dark(oklch(.4374 .0495 279.71), oklch(.8619 .0055 286.28));--colors-neutral-1100: light-dark(oklch(1 0 90), oklch(.2185 .0213 246.2deg));--colors-neutral: light-dark(oklch(.2737 .068 276.29), oklch(1 0 90));--colors-blue-400: light-dark(oklch(.9545 .0167 236.69), oklch(.2716 .0521 257.92));--colors-blue-500: light-dark(oklch(.9109 .0327 232.24), oklch(.3193 .0701 251.54));--colors-blue-600: light-dark(oklch(.8885 .0428 231.75), oklch(.3477 .0771 249.07));--colors-blue-1100: light-dark(oklch(.481 .1164 243.72), oklch(.6982 .1694 243.83));--colors-blue: light-dark(oklch(.5849 .1438 244.29), oklch(.6982 .1694 243.83));--colors-green-400: light-dark(oklch(.9637 .017 187.9), oklch(.2764 .0331 242.34));--colors-green-500: light-dark(oklch(.9307 .034 185.2), oklch(.333 .0416 210.14));--colors-green-600: light-dark(oklch(.9154 .0432 185.62), oklch(.3622 .0475 203.62));--colors-green-1100: light-dark(oklch(.5564 .0992 178.59), oklch(.755 .1426 170.23));--colors-green: light-dark(oklch(.6932 .1245 178.48), oklch(.755 .1426 170.23));--colors-orange-400: light-dark(oklch(.951 .0221 74.1), oklch(.2755 .014 3.07));--colors-orange-500: light-dark(oklch(.9396 .0436 71.7), oklch(.3344 .0381 61.07));--colors-orange-600: light-dark(oklch(.9251 .0549 71.49), oklch(.3634 .0513 63.86));--colors-orange-1100: light-dark(oklch(.6769 .1633 57), oklch(.772 .1738 64.55));--colors-orange: light-dark(oklch(.7387 .179 56.67), oklch(.772 .1738 64.55));--colors-red-400: light-dark(oklch(.9544 .0166 26.65), oklch(.2655 .0357 328.64));--colors-red-500: light-dark(oklch(.9112 .0328 27.11), oklch(.3103 .0508 358.44));--colors-red-600: light-dark(oklch(.8878 .0422 25.25), oklch(.3368 .0603 6.45));--colors-red-1100: light-dark(oklch(.515 .1713 30.54), oklch(.6881 .2018 30.03));--colors-red: light-dark(oklch(.598 .1886 30.3), oklch(.6881 .2018 30.03));--colors-gold-400: light-dark(oklch(.9765 .022 89.79), oklch(.2916 .009 340.92));--colors-gold-500: light-dark(oklch(.9556 .0434 91.27), oklch(.3564 .0252 69.32));--colors-gold-600: light-dark(oklch(.9434 .0539 92.15), oklch(.3918 .0369 76.19));--colors-gold-1100: light-dark(oklch(.6642 .1329 85.55), oklch(.8517 .1579 83.77));--colors-gold: light-dark(oklch(.7924 .1593 85.61), oklch(.8517 .1579 83.77));--colors-purple-400: light-dark(oklch(.9494 .0083 301.35), oklch(.2524 .0591 291.66));--colors-purple-500: light-dark(oklch(.8984 .0181 300.04), oklch(.2869 .0801 296.38));--colors-purple-600: light-dark(oklch(.8725 .0224 300.16), oklch(.3037 .0894 298.08));--colors-purple-1100: light-dark(oklch(.3841 .0825 296.42), oklch(.5483 .2189 304.41));--colors-purple: light-dark(oklch(.4629 .1027 296.59), oklch(.5483 .2189 304.41));--colors-neutral-gradient-from: light-dark(oklab(.2737 .0075 -.0676), oklab(.1553 .0542 -.0519));--colors-neutral-gradient-to: light-dark(oklab(.2018 .0685 -.0675), oklab(.2221 .0059 -.0521));--colors-neutral-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-neutral-gradient-from), var(--colors-neutral-gradient-to));--colors-blue-gradient-from: light-dark(oklab(.5849 -.0624 -.1296), oklab(.6982 -.0747 -.152));--colors-blue-gradient-to: light-dark(oklab(.5849 -.0624 -.1296), oklab(.6982 -.0747 -.152));--colors-blue-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-blue-gradient-from), var(--colors-blue-gradient-to));--colors-blue-gradient-darkened-from: light-dark(oklab(.4857 -.022 -.1807), oklab(.5755 -.048 -.1764));--colors-blue-gradient-darkened-to: light-dark(oklab(.5404 -.0523 -.1438), oklab(.6546 -.0695 -.1422));--colors-blue-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-blue-gradient-darkened-from), var(--colors-blue-gradient-darkened-to));--colors-green-gradient-from: light-dark(oklab(.6 -.1198 .0072), oklab(.65 -.1659 .0372));--colors-green-gradient-to: light-dark(oklab(.6932 -.1245 .0033), oklab(.755 -.1405 .0242));--colors-green-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-green-gradient-from), var(--colors-green-gradient-to));--colors-green-gradient-darkened-from: light-dark(oklab(.6231 -.0909 .0017), oklab(.7142 -.1422 .0319));--colors-green-gradient-darkened-to: light-dark(oklab(.6873 -.1184 -.0011), oklab(.6971 -.1282 .0212));--colors-green-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-green-gradient-darkened-from), var(--colors-green-gradient-darkened-to));--colors-orange-gradient-from: light-dark(oklab(.65 .1657 .1447), oklab(.7 .1105 .1667));--colors-orange-gradient-to: light-dark(oklab(.7387 .0984 .1496), oklab(.772 .0747 .1569));--colors-orange-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-orange-gradient-from), var(--colors-orange-gradient-to));--colors-orange-gradient-darkened-from: light-dark(oklab(.6387 .153 .1288), oklab(.7442 .1001 .1509));--colors-orange-gradient-darkened-to: light-dark(oklab(.7115 .1244 .144), oklab(.7438 .0703 .1509));--colors-orange-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-orange-gradient-darkened-from), var(--colors-orange-gradient-darkened-to));--colors-red-gradient-from: light-dark(oklab(.5 .2088 .0692), oklab(.6 .2142 .1288));--colors-red-gradient-to: light-dark(oklab(.598 .1628 .0952), oklab(.6881 .1747 .101));--colors-red-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-red-gradient-from), var(--colors-red-gradient-to));--colors-red-gradient-darkened-from: light-dark(oklab(.5344 .1734 .0523), oklab(.6515 .2016 .1213));--colors-red-gradient-darkened-to: light-dark(oklab(.5653 .177 .0874), oklab(.6633 .1597 .0937));--colors-red-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-red-gradient-darkened-from), var(--colors-red-gradient-darkened-to));--colors-gold-gradient-from: light-dark(oklab(.7 .0595 .1592), oklab(.78 .0503 .1624));--colors-gold-gradient-to: light-dark(oklab(.7924 .0122 .1588), oklab(.8517 .0171 .157));--colors-gold-gradient: radial-gradient(at 100% 100% in oklab, var(--colors-gold-gradient-from), var(--colors-gold-gradient-to));--colors-gold-gradient-darkened-from: light-dark(oklab(.7143 .0695 .139), oklab(.8163 .0458 .1479));--colors-gold-gradient-darkened-to: light-dark(oklab(.7575 .0315 .1515), oklab(.8221 .0167 .1498));--colors-gold-gradient-darkened: radial-gradient(at 100% 100% in oklab, var(--colors-gold-gradient-darkened-from), var(--colors-gold-gradient-darkened-to))}#feedback-widget .bg-gradient-gold{background-image:var(--colors-gold-gradient)}#feedback-widget .bg-gradient-green{background-image:var(--colors-green-gradient)}#feedback-widget .bg-gradient-red{background-image:var(--colors-red-gradient)}#feedback-widget{color-scheme:light dark}#feedback-widget .f-text-2xl,#feedback-widget [f-text-2xl=""]{--f-text-min:22;--f-text-max:26;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)))}#feedback-widget .f-text-sm,#feedback-widget [f-text-sm=""]{--f-text-min:14;--f-text-max:16;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)))}#feedback-widget .f-size-md{--f-size-min:24;--f-size-max:32;width:clamp(calc(var(--f-size-unit, 1px) * var(--f-size-min, 16)),calc(var(--f-size-unit, 1px) * var(--f-size-min, 16) + (var(--f-size-max, 16) - var(--f-size-min, 16)) * (var(--f-size-container, 100vw) - (var(--f-size-unit, 1px) * var(--f-size-min-container, 320))) / (var(--f-size-max-container, 1920) - var(--f-size-min-container, 320))),calc(var(--f-size-unit, 1px) * var(--f-size-max, 16)));height:clamp(calc(var(--f-size-unit, 1px) * var(--f-size-min, 16)),calc(var(--f-size-unit, 1px) * var(--f-size-min, 16) + (var(--f-size-max, 16) - var(--f-size-min, 16)) * (var(--f-size-container, 100vw) - (var(--f-size-unit, 1px) * var(--f-size-min-container, 320))) / (var(--f-size-max-container, 1920) - var(--f-size-min-container, 320))),calc(var(--f-size-unit, 1px) * var(--f-size-max, 16)))}#feedback-widget .f-px-xs,#feedback-widget [f-px-xs=""]{--f-px-min:12;--f-px-max:16;padding-left:clamp(calc(var(--f-px-unit, 1px) * var(--f-px-min, 16)),calc(var(--f-px-unit, 1px) * var(--f-px-min, 16) + (var(--f-px-max, 16) - var(--f-px-min, 16)) * (var(--f-px-container, 100vw) - (var(--f-px-unit, 1px) * var(--f-px-min-container, 320))) / (var(--f-px-max-container, 1920) - var(--f-px-min-container, 320))),calc(var(--f-px-unit, 1px) * var(--f-px-max, 16)));padding-right:clamp(calc(var(--f-px-unit, 1px) * var(--f-px-min, 16)),calc(var(--f-px-unit, 1px) * var(--f-px-min, 16) + (var(--f-px-max, 16) - var(--f-px-min, 16)) * (var(--f-px-container, 100vw) - (var(--f-px-unit, 1px) * var(--f-px-min-container, 320))) / (var(--f-px-max-container, 1920) - var(--f-px-min-container, 320))),calc(var(--f-px-unit, 1px) * var(--f-px-max, 16)))}#feedback-widget .f-mt-lg,#feedback-widget [f-mt-lg=""]{--f-mt-min:32;--f-mt-max:48;margin-top:clamp(calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16)),calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16) + (var(--f-mt-max, 16) - var(--f-mt-min, 16)) * (var(--f-mt-container, 100vw) - (var(--f-mt-unit, 1px) * var(--f-mt-min-container, 320))) / (var(--f-mt-max-container, 1920) - var(--f-mt-min-container, 320))),calc(var(--f-mt-unit, 1px) * var(--f-mt-max, 16)))}#feedback-widget .f-mt-md,#feedback-widget [f-mt-md=""]{--f-mt-min:24;--f-mt-max:32;margin-top:clamp(calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16)),calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16) + (var(--f-mt-max, 16) - var(--f-mt-min, 16)) * (var(--f-mt-container, 100vw) - (var(--f-mt-unit, 1px) * var(--f-mt-min-container, 320))) / (var(--f-mt-max-container, 1920) - var(--f-mt-min-container, 320))),calc(var(--f-mt-unit, 1px) * var(--f-mt-max, 16)))}#feedback-widget .f-mt-sm,#feedback-widget [f-mt-sm=""]{--f-mt-min:16;--f-mt-max:24;margin-top:clamp(calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16)),calc(var(--f-mt-unit, 1px) * var(--f-mt-min, 16) + (var(--f-mt-max, 16) - var(--f-mt-min, 16)) * (var(--f-mt-container, 100vw) - (var(--f-mt-unit, 1px) * var(--f-mt-min-container, 320))) / (var(--f-mt-max-container, 1920) - var(--f-mt-min-container, 320))),calc(var(--f-mt-unit, 1px) * var(--f-mt-max, 16)))}#feedback-widget .f-mb-md,#feedback-widget [f-mb-md=""]{--f-mb-min:24;--f-mb-max:32;margin-bottom:clamp(calc(var(--f-mb-unit, 1px) * var(--f-mb-min, 16)),calc(var(--f-mb-unit, 1px) * var(--f-mb-min, 16) + (var(--f-mb-max, 16) - var(--f-mb-min, 16)) * (var(--f-mb-container, 100vw) - (var(--f-mb-unit, 1px) * var(--f-mb-min-container, 320))) / (var(--f-mb-max-container, 1920) - var(--f-mb-min-container, 320))),calc(var(--f-mb-unit, 1px) * var(--f-mb-max, 16)))}#feedback-widget .f-p-2xs,#feedback-widget [f-p-2xs=""]{--f-p-min:8;--f-p-max:12;padding:clamp(calc(var(--f-p-unit, 1px) * var(--f-p-min, 16)),calc(var(--f-p-unit, 1px) * var(--f-p-min, 16) + (var(--f-p-max, 16) - var(--f-p-min, 16)) * (var(--f-p-container, 100vw) - (var(--f-p-unit, 1px) * var(--f-p-min-container, 320))) / (var(--f-p-max-container, 1920) - var(--f-p-min-container, 320))),calc(var(--f-p-unit, 1px) * var(--f-p-max, 16)))}#feedback-widget .f-p-md{--f-p-min:24;--f-p-max:32;padding:clamp(calc(var(--f-p-unit, 1px) * var(--f-p-min, 16)),calc(var(--f-p-unit, 1px) * var(--f-p-min, 16) + (var(--f-p-max, 16) - var(--f-p-min, 16)) * (var(--f-p-container, 100vw) - (var(--f-p-unit, 1px) * var(--f-p-min-container, 320))) / (var(--f-p-max-container, 1920) - var(--f-p-min-container, 320))),calc(var(--f-p-unit, 1px) * var(--f-p-max, 16)))}#feedback-widget .f-p-sm,#feedback-widget [f-p-sm=""]{--f-p-min:16;--f-p-max:24;padding:clamp(calc(var(--f-p-unit, 1px) * var(--f-p-min, 16)),calc(var(--f-p-unit, 1px) * var(--f-p-min, 16) + (var(--f-p-max, 16) - var(--f-p-min, 16)) * (var(--f-p-container, 100vw) - (var(--f-p-unit, 1px) * var(--f-p-min-container, 320))) / (var(--f-p-max-container, 1920) - var(--f-p-min-container, 320))),calc(var(--f-p-unit, 1px) * var(--f-p-max, 16)))}#feedback-widget .f-rounded-md{--f-rounded-min:6;--f-rounded-max:8;border-radius:clamp(calc(var(--f-rounded-unit, 1px) * var(--f-rounded-min, 16)),calc(var(--f-rounded-unit, 1px) * var(--f-rounded-min, 16) + (var(--f-rounded-max, 16) - var(--f-rounded-min, 16)) * (var(--f-rounded-container, 100vw) - (var(--f-rounded-unit, 1px) * var(--f-rounded-min-container, 320))) / (var(--f-rounded-max-container, 1920) - var(--f-rounded-min-container, 320))),calc(var(--f-rounded-unit, 1px) * var(--f-rounded-max, 16)))}#feedback-widget :where(.stack>*),#feedback-widget :where([stack=""]>*){grid-area:1 / 1;justify-self:center;align-self:center}#feedback-widget .stack,#feedback-widget [stack=""]{width:100%;display:grid;place-content:center;grid-template-columns:1fr;grid-template-rows:1fr}@property --nq-gradient-from{syntax: ""; inherits: false; initial-value: #0000;}@property --nq-gradient-to{syntax: ""; inherits: false; initial-value: #0000;}@property --nq-gradient-stops{syntax: "*"; inherits: false;}@keyframes mask-up{#feedback-widget 100% {mask-size: 100% var(--mask-size),100% 100%,100% var(--mask-size);}}@keyframes mask-down{#feedback-widget 100% {mask-size: 100% var(--mask-size),100% 100%,100% 0;}}#feedback-widget .nq-label{text-transform:uppercase;letter-spacing:.17em;font-size:.75em;font-weight:700;line-height:1;color:var(--colors-neutral-800)}#feedback-widget :where([data-inverted] *) :is(#feedback-widget .nq-label){color:color-mix(in oklch,var(--colors-neutral-0) 80%,transparent)}#feedback-widget :where(.dark,#feedback-widget [data-theme=dark]) :is(#feedback-widget .nq-label){color:color-mix(in oklch,var(--colors-white) 70%,transparent)}#feedback-widget [nq-label=""]{text-transform:uppercase;letter-spacing:.17em;font-size:.75em;font-weight:700;line-height:1;color:var(--colors-neutral-800)}#feedback-widget :where([data-inverted] *) :is(#feedback-widget [nq-label=""]){color:color-mix(in oklch,var(--colors-neutral-0) 80%,transparent)}#feedback-widget :where(.dark,#feedback-widget [data-theme=dark]) :is(#feedback-widget [nq-label=""]){color:color-mix(in oklch,var(--colors-white) 70%,transparent)}#feedback-widget .nq-pill-xl{position:relative;font-weight:700;font-size:.875rem;padding:.1875rem .8125rem;border:none;border-radius:999px;display:flex;align-items:center;justify-content:center;gap:.125rem;line-height:1.5;height:max-content;width:max-content;text-decoration:none;color:#fff;outline:none;background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);--transitions: --nq-gradient-from .2s var(--nq-ease), --nq-gradient-to .2s var(--nq-ease);transition:var(--transitions)}#feedback-widget :is(#feedback-widget .nq-pill-xl):where(.nq-pill-lg,#feedback-widget [nq-pill-lg]){font-size:1rem;padding:.3125rem 1rem}#feedback-widget :is(#feedback-widget .nq-pill-xl):where(.nq-pill-xl,#feedback-widget [nq-pill-xl]){box-shadow:0 .5rem 1.5rem #00000026;cursor:pointer;transition:transform .45s var(--nq-ease),box-shadow .45s var(--nq-ease),background-color .25s var(--nq-ease),var(--transitions);will-change:box-shadow;text-transform:uppercase;letter-spacing:.094em;min-width:12.5rem;flex:1;padding:1.25rem 2rem;line-height:1.25rem;font-size:1rem;gap:.5rem;border-radius:9999px;margin:1rem}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-pill-xl):where(.nq-pill-xl,#feedback-widget [nq-pill-xl])):where(:hover,#feedback-widget :focus){box-shadow:0 1rem 2.5rem #0003;transform:translate3D(0,-2px,0)}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-pill-xl):where(.nq-pill-xl,#feedback-widget [nq-pill-xl])):active{outline:none;box-shadow:0 .2rem .3rem #0003;transform:translate3D(0,1px,0);transition:transform .2s cubic-bezier(.41,.34,.26,1.55),box-shadow .2s cubic-bezier(.41,.34,.26,1.55),background-color .2s var(--nq-ease),var(--transitions)}#feedback-widget :is(#feedback-widget .nq-pill-xl):where([disabled]){pointer-events:none;cursor:not-allowed;opacity:.5}#feedback-widget :is(#feedback-widget .nq-pill-xl):not([disabled]):where(:hover,#feedback-widget :active,#feedback-widget :focus):before{opacity:1}#feedback-widget :is(#feedback-widget .nq-pill-xl):focus-visible{outline:2px solid var(--colors-blue);outline-offset:3px}#feedback-widget [nq-pill-xl=""]{position:relative;font-weight:700;font-size:.875rem;padding:.1875rem .8125rem;border:none;border-radius:999px;display:flex;align-items:center;justify-content:center;gap:.125rem;line-height:1.5;height:max-content;width:max-content;text-decoration:none;color:#fff;outline:none;background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);--transitions: --nq-gradient-from .2s var(--nq-ease), --nq-gradient-to .2s var(--nq-ease);transition:var(--transitions)}#feedback-widget :is(#feedback-widget [nq-pill-xl=""]):where(.nq-pill-lg,#feedback-widget [nq-pill-lg]){font-size:1rem;padding:.3125rem 1rem}#feedback-widget :is(#feedback-widget [nq-pill-xl=""]):where(.nq-pill-xl,#feedback-widget [nq-pill-xl]){box-shadow:0 .5rem 1.5rem #00000026;cursor:pointer;transition:transform .45s var(--nq-ease),box-shadow .45s var(--nq-ease),background-color .25s var(--nq-ease),var(--transitions);will-change:box-shadow;text-transform:uppercase;letter-spacing:.094em;min-width:12.5rem;flex:1;padding:1.25rem 2rem;line-height:1.25rem;font-size:1rem;gap:.5rem;border-radius:9999px;margin:1rem}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-pill-xl=""]):where(.nq-pill-xl,#feedback-widget [nq-pill-xl])):where(:hover,#feedback-widget :focus){box-shadow:0 1rem 2.5rem #0003;transform:translate3D(0,-2px,0)}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-pill-xl=""]):where(.nq-pill-xl,#feedback-widget [nq-pill-xl])):active{outline:none;box-shadow:0 .2rem .3rem #0003;transform:translate3D(0,1px,0);transition:transform .2s cubic-bezier(.41,.34,.26,1.55),box-shadow .2s cubic-bezier(.41,.34,.26,1.55),background-color .2s var(--nq-ease),var(--transitions)}#feedback-widget :is(#feedback-widget [nq-pill-xl=""]):where([disabled]){pointer-events:none;cursor:not-allowed;opacity:.5}#feedback-widget :is(#feedback-widget [nq-pill-xl=""]):not([disabled]):where(:hover,#feedback-widget :active,#feedback-widget :focus):before{opacity:1}#feedback-widget :is(#feedback-widget [nq-pill-xl=""]):focus-visible{outline:2px solid var(--colors-blue);outline-offset:3px}#feedback-widget .nq-pill-blue{position:relative;font-weight:700;font-size:.875rem;padding:.1875rem .8125rem;border:none;border-radius:999px;display:flex;align-items:center;justify-content:center;gap:.125rem;line-height:1.5;height:max-content;width:max-content;text-decoration:none;color:#fff;outline:none;background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);--transitions: --nq-gradient-from .2s var(--nq-ease), --nq-gradient-to .2s var(--nq-ease);transition:var(--transitions);--nq-gradient-from: var(--colors-blue-gradient-from);--nq-gradient-to: var(--colors-blue-gradient-to)}#feedback-widget :is(#feedback-widget .nq-pill-blue):where(.nq-pill-lg,#feedback-widget [nq-pill-lg]){font-size:1rem;padding:.3125rem 1rem}#feedback-widget :is(#feedback-widget .nq-pill-blue):where(.nq-pill-xl,#feedback-widget [nq-pill-xl]){box-shadow:0 .5rem 1.5rem #00000026;cursor:pointer;transition:transform .45s var(--nq-ease),box-shadow .45s var(--nq-ease),background-color .25s var(--nq-ease),var(--transitions);will-change:box-shadow;text-transform:uppercase;letter-spacing:.094em;min-width:12.5rem;flex:1;padding:1.25rem 2rem;line-height:1.25rem;font-size:1rem;gap:.5rem;border-radius:9999px;margin:1rem}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-pill-blue):where(.nq-pill-xl,#feedback-widget [nq-pill-xl])):where(:hover,#feedback-widget :focus){box-shadow:0 1rem 2.5rem #0003;transform:translate3D(0,-2px,0)}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-pill-blue):where(.nq-pill-xl,#feedback-widget [nq-pill-xl])):active{outline:none;box-shadow:0 .2rem .3rem #0003;transform:translate3D(0,1px,0);transition:transform .2s cubic-bezier(.41,.34,.26,1.55),box-shadow .2s cubic-bezier(.41,.34,.26,1.55),background-color .2s var(--nq-ease),var(--transitions)}#feedback-widget :is(#feedback-widget .nq-pill-blue):where([disabled]){pointer-events:none;cursor:not-allowed;opacity:.5}#feedback-widget :is(#feedback-widget .nq-pill-blue):not([disabled]):where(:hover,#feedback-widget :active,#feedback-widget :focus):before{opacity:1}#feedback-widget :is(#feedback-widget .nq-pill-blue):focus-visible{outline:2px solid var(--colors-blue);outline-offset:3px}#feedback-widget :is(#feedback-widget .nq-pill-blue):hover,#feedback-widget :is(#feedback-widget .nq-pill-blue):focus-visible{--nq-gradient-from: var(--colors-blue-gradient-darkened-from);--nq-gradient-to: var(--colors-blue-gradient-darkened-to)}#feedback-widget [nq-pill-blue=""]{position:relative;font-weight:700;font-size:.875rem;padding:.1875rem .8125rem;border:none;border-radius:999px;display:flex;align-items:center;justify-content:center;gap:.125rem;line-height:1.5;height:max-content;width:max-content;text-decoration:none;color:#fff;outline:none;background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);--transitions: --nq-gradient-from .2s var(--nq-ease), --nq-gradient-to .2s var(--nq-ease);transition:var(--transitions);--nq-gradient-from: var(--colors-blue-gradient-from);--nq-gradient-to: var(--colors-blue-gradient-to)}#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):where(.nq-pill-lg,#feedback-widget [nq-pill-lg]){font-size:1rem;padding:.3125rem 1rem}#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):where(.nq-pill-xl,#feedback-widget [nq-pill-xl]){box-shadow:0 .5rem 1.5rem #00000026;cursor:pointer;transition:transform .45s var(--nq-ease),box-shadow .45s var(--nq-ease),background-color .25s var(--nq-ease),var(--transitions);will-change:box-shadow;text-transform:uppercase;letter-spacing:.094em;min-width:12.5rem;flex:1;padding:1.25rem 2rem;line-height:1.25rem;font-size:1rem;gap:.5rem;border-radius:9999px;margin:1rem}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):where(.nq-pill-xl,#feedback-widget [nq-pill-xl])):where(:hover,#feedback-widget :focus){box-shadow:0 1rem 2.5rem #0003;transform:translate3D(0,-2px,0)}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):where(.nq-pill-xl,#feedback-widget [nq-pill-xl])):active{outline:none;box-shadow:0 .2rem .3rem #0003;transform:translate3D(0,1px,0);transition:transform .2s cubic-bezier(.41,.34,.26,1.55),box-shadow .2s cubic-bezier(.41,.34,.26,1.55),background-color .2s var(--nq-ease),var(--transitions)}#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):where([disabled]){pointer-events:none;cursor:not-allowed;opacity:.5}#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):not([disabled]):where(:hover,#feedback-widget :active,#feedback-widget :focus):before{opacity:1}#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):focus-visible{outline:2px solid var(--colors-blue);outline-offset:3px}#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):hover,#feedback-widget :is(#feedback-widget [nq-pill-blue=""]):focus-visible{--nq-gradient-from: var(--colors-blue-gradient-darkened-from);--nq-gradient-to: var(--colors-blue-gradient-darkened-to)}#feedback-widget :is(#feedback-widget .nq-switch):where(input[type=checkbox]){--active-color: var(--colors-neutral-300);appearance:none;font-size:1em;width:2em;height:max-content;aspect-ratio:1.625;border-radius:2em;background:var(--active-color);position:relative;cursor:pointer}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-switch):where(input[type=checkbox])):before{content:"";position:absolute;transform:translate(-50%,-50%);top:50%;left:.6em;width:.9em;height:.9em;border:1px solid color-mix(in oklch,var(--colors-neutral) 2%,transparent);border-radius:50%;background:#fff;transition:left .1s ease-out;box-shadow:var(--nq-shadow)}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-switch):where(input[type=checkbox])):checked{--active-color: var(--colors-blue)}#feedback-widget :is(#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-switch):where(input[type=checkbox])):checked):before{left:1.4em}@media (prefers-reduced-motion){#feedback-widget :is(#feedback-widget .nq-switch):where(input[type=checkbox]){transition-duration:0s}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-switch):where(input[type=checkbox])):before{transition-duration:0s}}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-switch):where(input[type=checkbox]))[disabled]{opacity:.6;cursor:not-allowed}#feedback-widget :is(#feedback-widget [nq-switch=""]):where(input[type=checkbox]){--active-color: var(--colors-neutral-300);appearance:none;font-size:1em;width:2em;height:max-content;aspect-ratio:1.625;border-radius:2em;background:var(--active-color);position:relative;cursor:pointer}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-switch=""]):where(input[type=checkbox])):before{content:"";position:absolute;transform:translate(-50%,-50%);top:50%;left:.6em;width:.9em;height:.9em;border:1px solid color-mix(in oklch,var(--colors-neutral) 2%,transparent);border-radius:50%;background:#fff;transition:left .1s ease-out;box-shadow:var(--nq-shadow)}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-switch=""]):where(input[type=checkbox])):checked{--active-color: var(--colors-blue)}#feedback-widget :is(#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-switch=""]):where(input[type=checkbox])):checked):before{left:1.4em}@media (prefers-reduced-motion){#feedback-widget :is(#feedback-widget [nq-switch=""]):where(input[type=checkbox]){transition-duration:0s}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-switch=""]):where(input[type=checkbox])):before{transition-duration:0s}}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-switch=""]):where(input[type=checkbox]))[disabled]{opacity:.6;cursor:not-allowed}#feedback-widget .nq-input-box{font-size:1em;line-height:1.5;padding:.28125em .875em;height:max-content;min-width:min(8em,220px);border-radius:.125em;width:100%;background-color:transparent;transition:colors .2s var(--nq-ease),box-shadow .1s var(--nq-ease);--color: var(--colors-neutral);--placeholder-color: var(--colors-neutral-700);--outline-color: var(--colors-neutral-500);outline:1.5px solid var(--outline-color);color:var(--color)}#feedback-widget :where([data-inverted] *) :is(#feedback-widget .nq-input-box){--placeholder-color: color-mix(in oklch, var(--colors-neutral-0) 80%, transparent)}#feedback-widget :is(#feedback-widget .nq-input-box)::placeholder{color:var(--placeholder-color)}#feedback-widget :is(#feedback-widget .nq-input-box):not(.nq-invalid,#feedback-widget :user-invalid):where(:hover,#feedback-widget :focus,#feedback-widget :focus-within){--color: var(--colors-blue);--outline-color: color-mix(in oklch, var(--colors-blue) 30%, transparent)}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-input-box):not(.nq-invalid,#feedback-widget :user-invalid):where(:hover,#feedback-widget :focus,#feedback-widget :focus-within))::placeholder{--placeholder-color: color-mix(in oklch, var(--colors-blue) 60%, transparent)}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-input-box):not(.nq-invalid,#feedback-widget :user-invalid):where(:hover,#feedback-widget :focus,#feedback-widget :focus-within)):focus:valid,#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-input-box):not(.nq-invalid,#feedback-widget :user-invalid):where(:hover,#feedback-widget :focus,#feedback-widget :focus-within)):focus-within:valid{--outline-color: var(--colors-blue)}#feedback-widget :is(#feedback-widget .nq-input-box):where(.nq-invalid,#feedback-widget :user-invalid){--color: var(--colors-orange);outline-color:color-mix(in oklch,var(--colors-orange) 40%,transparent)}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-input-box):where(.nq-invalid,#feedback-widget :user-invalid))::placeholder{--placeholder-color: color-mix(in oklch, var(--colors-orange) 70%, transparent)}#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-input-box):where(.nq-invalid,#feedback-widget :user-invalid)):hover,#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-input-box):where(.nq-invalid,#feedback-widget :user-invalid)):focus,#feedback-widget :is(#feedback-widget :is(#feedback-widget .nq-input-box):where(.nq-invalid,#feedback-widget :user-invalid)):focus-within{outline-color:var(--colors-orange)}#feedback-widget :is(#feedback-widget .nq-input-box):where(textarea,#feedback-widget :has(textarea)){--padding: .25em;min-height:calc(3lh + 2 * var(--padding));max-height:calc(5lh + 2 * var(--padding));field-sizing:content;padding:var(--padding) calc(var(--padding) + ((1lh - 1ex) / 2))}#feedback-widget [nq-input-box=""]{font-size:1em;line-height:1.5;padding:.28125em .875em;height:max-content;min-width:min(8em,220px);border-radius:.125em;width:100%;background-color:transparent;transition:colors .2s var(--nq-ease),box-shadow .1s var(--nq-ease);--color: var(--colors-neutral);--placeholder-color: var(--colors-neutral-700);--outline-color: var(--colors-neutral-500);outline:1.5px solid var(--outline-color);color:var(--color)}#feedback-widget :where([data-inverted] *) :is(#feedback-widget [nq-input-box=""]){--placeholder-color: color-mix(in oklch, var(--colors-neutral-0) 80%, transparent)}#feedback-widget :is(#feedback-widget [nq-input-box=""])::placeholder{color:var(--placeholder-color)}#feedback-widget :is(#feedback-widget [nq-input-box=""]):not(.nq-invalid,#feedback-widget :user-invalid):where(:hover,#feedback-widget :focus,#feedback-widget :focus-within){--color: var(--colors-blue);--outline-color: color-mix(in oklch, var(--colors-blue) 30%, transparent)}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-input-box=""]):not(.nq-invalid,#feedback-widget :user-invalid):where(:hover,#feedback-widget :focus,#feedback-widget :focus-within))::placeholder{--placeholder-color: color-mix(in oklch, var(--colors-blue) 60%, transparent)}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-input-box=""]):not(.nq-invalid,#feedback-widget :user-invalid):where(:hover,#feedback-widget :focus,#feedback-widget :focus-within)):focus:valid,#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-input-box=""]):not(.nq-invalid,#feedback-widget :user-invalid):where(:hover,#feedback-widget :focus,#feedback-widget :focus-within)):focus-within:valid{--outline-color: var(--colors-blue)}#feedback-widget :is(#feedback-widget [nq-input-box=""]):where(.nq-invalid,#feedback-widget :user-invalid){--color: var(--colors-orange);outline-color:color-mix(in oklch,var(--colors-orange) 40%,transparent)}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-input-box=""]):where(.nq-invalid,#feedback-widget :user-invalid))::placeholder{--placeholder-color: color-mix(in oklch, var(--colors-orange) 70%, transparent)}#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-input-box=""]):where(.nq-invalid,#feedback-widget :user-invalid)):hover,#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-input-box=""]):where(.nq-invalid,#feedback-widget :user-invalid)):focus,#feedback-widget :is(#feedback-widget :is(#feedback-widget [nq-input-box=""]):where(.nq-invalid,#feedback-widget :user-invalid)):focus-within{outline-color:var(--colors-orange)}#feedback-widget :is(#feedback-widget [nq-input-box=""]):where(textarea,#feedback-widget :has(textarea)){--padding: .25em;min-height:calc(3lh + 2 * var(--padding));max-height:calc(5lh + 2 * var(--padding));field-sizing:content;padding:var(--padding) calc(var(--padding) + ((1lh - 1ex) / 2))}#feedback-widget .nq-hoverable-green{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-green-gradient-from);--nq-gradient-to: var(--colors-green-gradient-to)}@media (hover: hover){#feedback-widget :is(#feedback-widget .nq-hoverable-green):where(:hover,#feedback-widget :focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--hoverable-text, var(--colors-darkblue));box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}#feedback-widget .nq-hoverable-green #feedback-widget>*{z-index:1}}#feedback-widget :is(#feedback-widget .nq-hoverable-green):hover,#feedback-widget :is(#feedback-widget .nq-hoverable-green):focus-visible{--nq-gradient-from: var(--colors-green-gradient-darkened-from);--nq-gradient-to: var(--colors-green-gradient-darkened-to);--hoverable-text: var(--colors-white)}#feedback-widget [nq-hoverable-green=""]{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-green-gradient-from);--nq-gradient-to: var(--colors-green-gradient-to)}@media (hover: hover){#feedback-widget :is(#feedback-widget [nq-hoverable-green=""]):where(:hover,#feedback-widget :focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--hoverable-text, var(--colors-darkblue));box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}#feedback-widget [nq-hoverable-green=""] #feedback-widget>*{z-index:1}}#feedback-widget :is(#feedback-widget [nq-hoverable-green=""]):hover,#feedback-widget :is(#feedback-widget [nq-hoverable-green=""]):focus-visible{--nq-gradient-from: var(--colors-green-gradient-darkened-from);--nq-gradient-to: var(--colors-green-gradient-darkened-to);--hoverable-text: var(--colors-white)}#feedback-widget .nq-hoverable-red{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-red-gradient-from);--nq-gradient-to: var(--colors-red-gradient-to)}@media (hover: hover){#feedback-widget :is(#feedback-widget .nq-hoverable-red):where(:hover,#feedback-widget :focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--hoverable-text, var(--colors-darkblue));box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}#feedback-widget .nq-hoverable-red #feedback-widget>*{z-index:1}}#feedback-widget :is(#feedback-widget .nq-hoverable-red):hover,#feedback-widget :is(#feedback-widget .nq-hoverable-red):focus-visible{--nq-gradient-from: var(--colors-red-gradient-darkened-from);--nq-gradient-to: var(--colors-red-gradient-darkened-to);--hoverable-text: var(--colors-white)}#feedback-widget [nq-hoverable-red=""]{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-red-gradient-from);--nq-gradient-to: var(--colors-red-gradient-to)}@media (hover: hover){#feedback-widget :is(#feedback-widget [nq-hoverable-red=""]):where(:hover,#feedback-widget :focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--hoverable-text, var(--colors-darkblue));box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}#feedback-widget [nq-hoverable-red=""] #feedback-widget>*{z-index:1}}#feedback-widget :is(#feedback-widget [nq-hoverable-red=""]):hover,#feedback-widget :is(#feedback-widget [nq-hoverable-red=""]):focus-visible{--nq-gradient-from: var(--colors-red-gradient-darkened-from);--nq-gradient-to: var(--colors-red-gradient-darkened-to);--hoverable-text: var(--colors-white)}#feedback-widget .nq-hoverable-gold{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-gold-gradient-from);--nq-gradient-to: var(--colors-gold-gradient-to)}@media (hover: hover){#feedback-widget :is(#feedback-widget .nq-hoverable-gold):where(:hover,#feedback-widget :focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--hoverable-text, var(--colors-darkblue));box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}#feedback-widget .nq-hoverable-gold #feedback-widget>*{z-index:1}}#feedback-widget :is(#feedback-widget .nq-hoverable-gold):hover,#feedback-widget :is(#feedback-widget .nq-hoverable-gold):focus-visible{--nq-gradient-from: var(--colors-gold-gradient-darkened-from);--nq-gradient-to: var(--colors-gold-gradient-darkened-to);--hoverable-text: var(--colors-white)}#feedback-widget [nq-hoverable-gold=""]{--nq-gradient-from: var(--colors-neutral-300);--nq-gradient-to: color-mix(in oklch, var(--colors-neutral-300) 65%, transparent);background-image:radial-gradient(at 100% 100% in oklab,var(--nq-gradient-from) 0%,var(--nq-gradient-to) 100%);color:var(--colors-neutral);border-radius:.5rem;outline:1.5px solid color-mix(in oklch,var(--colors-neutral) 4%,transparent);outline-offset:-1.5px;display:flex;flex-direction:column;overflow:hidden;position:relative;padding:1.5rem;cursor:pointer;transition:--nq-gradient-from .3s var(--nq-ease),--nq-gradient-to .3s var(--nq-ease),transform .2s ease-out,box-shadow .2s ease-out,color .2s ease-out,outline-color .2s ease-out;--nq-gradient-from: var(--colors-gold-gradient-from);--nq-gradient-to: var(--colors-gold-gradient-to)}@media (hover: hover){#feedback-widget :is(#feedback-widget [nq-hoverable-gold=""]):where(:hover,#feedback-widget :focus-visible){--nq-gradient-from: var(--colors-white);--nq-gradient-to: var(--colors-white);color:var(--hoverable-text, var(--colors-darkblue));box-shadow:var(--nq-shadow);outline-color:color-mix(in oklch,var(--colors-neutral) 2%,transparent);transform:translateY(-.375rem)}#feedback-widget [nq-hoverable-gold=""] #feedback-widget>*{z-index:1}}#feedback-widget :is(#feedback-widget [nq-hoverable-gold=""]):hover,#feedback-widget :is(#feedback-widget [nq-hoverable-gold=""]):focus-visible{--nq-gradient-from: var(--colors-gold-gradient-darkened-from);--nq-gradient-to: var(--colors-gold-gradient-darkened-to);--hoverable-text: var(--colors-white)}#feedback-widget .text-14,#feedback-widget [text-14=""]{font-size:.875rem}#feedback-widget [text~="10"]{font-size:.625rem}#feedback-widget [text~="12"]{font-size:.75rem}#feedback-widget [text~="24"]{font-size:1.5rem}#feedback-widget :where(.group,#feedback-widget [group]):hover [text~="group-hocus:neutral-800"],#feedback-widget :where(.group,#feedback-widget [group]):focus-visible [text~="group-hocus:neutral-800"],#feedback-widget .text-neutral-800,#feedback-widget [text-neutral-800=""],#feedback-widget [text~=neutral-800]{color:color-mix(in srgb,var(--colors-neutral-800) var(--un-text-opacity),transparent)}#feedback-widget .text-neutral-700,#feedback-widget [text-neutral-700=""],#feedback-widget [text~=neutral-700]{color:color-mix(in srgb,var(--colors-neutral-700) var(--un-text-opacity),transparent)}#feedback-widget .text-orange,#feedback-widget [text-orange=""]{color:color-mix(in srgb,var(--colors-orange-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget .text-white,#feedback-widget [text-white=""]{color:color-mix(in srgb,var(--colors-white) var(--un-text-opacity),transparent)}#feedback-widget [text~="[&:has(~_label:hover)]:gold"]:has(~label:hover),#feedback-widget [text~="data-[state=active]:gold"][data-state=active],#feedback-widget [text~="hocus:gold"]:hover,#feedback-widget [text~="hocus:gold"]:focus-visible{color:color-mix(in srgb,var(--colors-gold-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget [text~=neutral-300]{color:color-mix(in srgb,var(--colors-neutral-300) var(--un-text-opacity),transparent)}#feedback-widget [text~=neutral]{color:color-mix(in srgb,var(--colors-neutral-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget [text~=red-1100]{color:color-mix(in srgb,var(--colors-red-1100) var(--un-text-opacity),transparent)}#feedback-widget [un-text-current=""]{color:currentColor}#feedback-widget .lh-24{--un-leading:calc(var(--spacing) * 24);line-height:calc(var(--spacing) * 24)}#feedback-widget .lh-none,#feedback-widget [lh-none=""]{--un-leading:var(--leading-none);line-height:var(--leading-none)}#feedback-widget .font-bold,#feedback-widget [font-bold=""]{--un-font-weight:var(--fontWeight-bold);font-weight:var(--fontWeight-bold)}#feedback-widget .font-mono,#feedback-widget [font-mono=""]{font-family:var(--font-mono)}#feedback-widget .font-normal,#feedback-widget [font-normal=""]{--un-font-weight:var(--fontWeight-normal);font-weight:var(--fontWeight-normal)}#feedback-widget .font-semibold,#feedback-widget [font-semibold=""]{--un-font-weight:var(--fontWeight-semibold);font-weight:var(--fontWeight-semibold)}#feedback-widget .mx-0,#feedback-widget [mx-0=""]{margin-inline:calc(var(--spacing) * 0)}#feedback-widget .mx-8,#feedback-widget [mx-8=""]{margin-inline:calc(var(--spacing) * 8)}#feedback-widget .mx-auto,#feedback-widget [mx-auto=""]{margin-inline:auto}#feedback-widget .mb-0,#feedback-widget [mb-0=""]{margin-bottom:calc(var(--spacing) * 0)}#feedback-widget .mb-12,#feedback-widget [mb-12=""]{margin-bottom:calc(var(--spacing) * 12)}#feedback-widget .mb-16,#feedback-widget [mb-16=""]{margin-bottom:calc(var(--spacing) * 16)}#feedback-widget .mb-8,#feedback-widget [mb-8=""]{margin-bottom:calc(var(--spacing) * 8)}#feedback-widget .mt--8,#feedback-widget [mt--8=""]{margin-top:calc(var(--spacing) * -8)}#feedback-widget .mt-1,#feedback-widget [mt-1=""]{margin-top:calc(var(--spacing) * 1)}#feedback-widget .mt-2,#feedback-widget [mt-2=""]{margin-top:calc(var(--spacing) * 2)}#feedback-widget .mt-6,#feedback-widget [mt-6=""]{margin-top:calc(var(--spacing) * 6)}#feedback-widget .mt-auto,#feedback-widget [mt-auto=""]{margin-top:auto}#feedback-widget .px-1\.5,#feedback-widget [px-1\.5=""]{padding-inline:calc(var(--spacing) * 1.5)}#feedback-widget .px-2,#feedback-widget [px-2=""]{padding-inline:calc(var(--spacing) * 2)}#feedback-widget [text~=center]{text-align:center}#feedback-widget .outline-1\.5,#feedback-widget [outline~="[&:has(:focus-visible)]:1.5"]:has(:focus-visible),#feedback-widget [outline~="1.5"]{outline-style:var(--un-outline-style);outline-width:1.5px}#feedback-widget .outline-neutral-200{outline-color:color-mix(in srgb,var(--colors-neutral-200) var(--un-outline-opacity),transparent)}#feedback-widget [outline~=blue]{outline-color:color-mix(in srgb,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}#feedback-widget [outline~="neutral/15"]{outline-color:color-mix(in srgb,var(--colors-neutral-DEFAULT) 15%,transparent)}#feedback-widget [outline~=red-500]{outline-color:color-mix(in srgb,var(--colors-red-500) var(--un-outline-opacity),transparent)}#feedback-widget [outline~="white/8"]{outline-color:color-mix(in srgb,var(--colors-white) 8%,transparent)}#feedback-widget .focus-visible\:outline-blue:focus-visible{outline-color:color-mix(in srgb,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}#feedback-widget [focus-visible\:outline-blue=""]:focus-visible{outline-color:color-mix(in srgb,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}#feedback-widget [outline~="offset--1.5"]{outline-offset:-1.5px}#feedback-widget .outline{outline-style:var(--un-outline-style);outline-width:1px}#feedback-widget .list-disc,#feedback-widget [list-disc=""]{list-style-type:disc}#feedback-widget .border{border-width:1px}#feedback-widget .border-transparent{border-color:transparent}#feedback-widget [border-transparent~="!"]{border-color:transparent!important}#feedback-widget .rounded-3,#feedback-widget [rounded-3=""]{border-radius:.1875rem}#feedback-widget .rounded-4,#feedback-widget [rounded-4=""]{border-radius:.25rem}#feedback-widget .rounded-6,#feedback-widget [rounded-6=""]{border-radius:.375rem}#feedback-widget .rounded-full,#feedback-widget [rounded-full=""]{border-radius:calc(infinity * 1px)}#feedback-widget .border-none{--un-border-style:none;border-style:none}#feedback-widget .bg-neutral-100,#feedback-widget [bg-neutral-100=""]{background-color:color-mix(in srgb,var(--colors-neutral-100) var(--un-bg-opacity),transparent)}#feedback-widget .bg-red-400,#feedback-widget [bg-red-400=""]{background-color:color-mix(in srgb,var(--colors-red-400) var(--un-bg-opacity),transparent)}#feedback-widget .bg-white,#feedback-widget [bg-white=""]{background-color:color-mix(in srgb,var(--colors-white) var(--un-bg-opacity),transparent)}#feedback-widget .op-0{opacity:0%}#feedback-widget .op-100{opacity:100%}#feedback-widget .op-90,#feedback-widget [op-90=""]{opacity:90%}#feedback-widget .disabled\:op-60:disabled{opacity:60%}#feedback-widget [disabled\:op-60=""]:disabled{opacity:60%}#feedback-widget .underline,#feedback-widget [underline=""]{text-decoration-line:underline}#feedback-widget .flex,#feedback-widget [flex=""],#feedback-widget [flex~="~"]{display:flex}#feedback-widget .shrink-0,#feedback-widget [shrink-0=""]{flex-shrink:0}#feedback-widget .flex-col,#feedback-widget [flex~=col]{flex-direction:column}#feedback-widget .gap-12,#feedback-widget [flex~=gap-12]{gap:calc(var(--spacing) * 12)}#feedback-widget .gap-16,#feedback-widget [flex~=gap-16],#feedback-widget [grid~=gap-16]{gap:calc(var(--spacing) * 16)}#feedback-widget .gap-24,#feedback-widget [flex~=gap-24]{gap:calc(var(--spacing) * 24)}#feedback-widget .gap-8,#feedback-widget [flex~=gap-8]{gap:calc(var(--spacing) * 8)}#feedback-widget .grid,#feedback-widget [grid=""],#feedback-widget [grid~="~"]{display:grid}#feedback-widget .col-span-2,#feedback-widget [col-span-2=""]{grid-column:span 2/span 2}#feedback-widget .cols-\[repeat\(auto-fit\, #feedback-widget 128px\)\],#feedback-widget [grid~="cols-[repeat(auto-fit, #feedback-widget 128px)]"]{grid-template-columns:repeat(auto-fit,128px)}#feedback-widget .cols-2,#feedback-widget [grid~=cols-2]{grid-template-columns:repeat(2,minmax(0,1fr))}#feedback-widget .rows-2,#feedback-widget [grid~=rows-2]{grid-template-rows:repeat(2,minmax(0,1fr))}#feedback-widget [rows~="4"]{grid-template-rows:repeat(4,minmax(0,1fr))}#feedback-widget .size-128,#feedback-widget [size-128=""]{width:calc(var(--spacing) * 128);height:calc(var(--spacing) * 128)}#feedback-widget .size-128\!{width:calc(var(--spacing) * 128)!important;height:calc(var(--spacing) * 128)!important}#feedback-widget .size-24,#feedback-widget [size-24=""]{width:calc(var(--spacing) * 24);height:calc(var(--spacing) * 24)}#feedback-widget .size-40,#feedback-widget [size-40=""],#feedback-widget [active~=size-40]:active{width:calc(var(--spacing) * 40);height:calc(var(--spacing) * 40)}#feedback-widget .h-1lh,#feedback-widget [h-1lh=""]{height:1lh}#feedback-widget .h-full,#feedback-widget [h-full=""]{height:100%}#feedback-widget .h-max,#feedback-widget [h-max=""]{height:max-content}#feedback-widget .h2{height:calc(var(--spacing) * 2)}#feedback-widget .h3{height:calc(var(--spacing) * 3)}#feedback-widget .max-h-128,#feedback-widget [max-h-128=""]{max-height:calc(var(--spacing) * 128)}#feedback-widget .w-auto,#feedback-widget [w-auto=""]{width:auto}#feedback-widget .w-full,#feedback-widget [w-full=""]{width:100%}#feedback-widget .w-max,#feedback-widget [w-max=""]{width:max-content}#feedback-widget [h-full~="!"]{height:100%!important}#feedback-widget .aspect-square,#feedback-widget [aspect-square=""]{aspect-ratio:1/1}#feedback-widget .hidden,#feedback-widget [hidden=""]{display:none}#feedback-widget .cursor-pointer,#feedback-widget [cursor-pointer=""],#feedback-widget [active~=cursor-pointer]:active{cursor:pointer}#feedback-widget .select-none,#feedback-widget [select-none=""]{-webkit-user-select:none;user-select:none}#feedback-widget .text-balance,#feedback-widget [text-balance=""]{text-wrap:balance}#feedback-widget .uppercase{text-transform:uppercase}#feedback-widget .shadow,#feedback-widget [shadow=""]{--un-shadow:var(--nq-shadow);box-shadow:var(--un-inset-shadow),var(--un-inset-ring-shadow),var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}#feedback-widget .transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration))}#feedback-widget .transition-colors,#feedback-widget [transition-colors=""]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration))}#feedback-widget .transition-opacity{transition-property:opacity;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration))}#feedback-widget [active~=transition-colors]:active{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration))}#feedback-widget .duration-200{--un-duration:.2s;transition-duration:.2s}#feedback-widget :where(.group,#feedback-widget [group]):hover [delay~="group-hocus:[calc(var(--b)*(5-var(--i)))]"],#feedback-widget :where(.group,#feedback-widget [group]):focus-visible [delay~="group-hocus:[calc(var(--b)*(5-var(--i)))]"]{transition-delay:calc(var(--b) * (5 - var(--i)))}#feedback-widget [delay~="[calc(25ms*var(--i))]"]{transition-delay:calc(25ms * var(--i))}#feedback-widget .items-start,#feedback-widget [flex~=items-start]{align-items:flex-start}#feedback-widget .items-center,#feedback-widget [flex~=items-center]{align-items:center}#feedback-widget .self-start,#feedback-widget [self-start=""]{align-self:flex-start}#feedback-widget .justify-center,#feedback-widget [flex~=justify-center]{justify-content:center}#feedback-widget .right--12,#feedback-widget [right--12=""]{right:calc(var(--spacing) * -12)}#feedback-widget .top--12,#feedback-widget [top--12=""]{top:calc(var(--spacing) * -12)}#feedback-widget .justify-self-end,#feedback-widget [justify-self-end=""]{justify-self:end}#feedback-widget .sr-only,#feedback-widget [sr-only=""]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}#feedback-widget .object-contain,#feedback-widget [object-contain=""]{object-fit:contain}#feedback-widget [text~=f-sm]{--f-text-min:14;--f-text-max:16;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)))}#feedback-widget [text~=f-xs]{--f-text-min:12;--f-text-max:14;font-size:clamp(calc(var(--f-text-unit, 1px) * var(--f-text-min, 16)),calc(var(--f-text-unit, 1px) * var(--f-text-min, 16) + (var(--f-text-max, 16) - var(--f-text-min, 16)) * (var(--f-text-container, 100vw) - (var(--f-text-unit, 1px) * var(--f-text-min-container, 320))) / (var(--f-text-max-container, 1920) - var(--f-text-min-container, 320))),calc(var(--f-text-unit, 1px) * var(--f-text-max, 16)))}@supports (color: color-mix(in lab,red,red)){#feedback-widget :where(.group,#feedback-widget [group]):hover [text~="group-hocus:neutral-800"],#feedback-widget :where(.group,#feedback-widget [group]):focus-visible [text~="group-hocus:neutral-800"]{color:color-mix(in oklab,var(--colors-neutral-800) var(--un-text-opacity),transparent)}#feedback-widget .text-neutral-700{color:color-mix(in oklab,var(--colors-neutral-700) var(--un-text-opacity),transparent)}#feedback-widget .text-neutral-800{color:color-mix(in oklab,var(--colors-neutral-800) var(--un-text-opacity),transparent)}#feedback-widget .text-orange{color:color-mix(in oklab,var(--colors-orange-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget .text-white{color:color-mix(in oklab,var(--colors-white) var(--un-text-opacity),transparent)}#feedback-widget [text-neutral-700=""]{color:color-mix(in oklab,var(--colors-neutral-700) var(--un-text-opacity),transparent)}#feedback-widget [text-neutral-800=""]{color:color-mix(in oklab,var(--colors-neutral-800) var(--un-text-opacity),transparent)}#feedback-widget [text-orange=""]{color:color-mix(in oklab,var(--colors-orange-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget [text-white=""]{color:color-mix(in oklab,var(--colors-white) var(--un-text-opacity),transparent)}#feedback-widget [text~="[&:has(~_label:hover)]:gold"]:has(~label:hover){color:color-mix(in oklab,var(--colors-gold-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget [text~="data-[state=active]:gold"][data-state=active]{color:color-mix(in oklab,var(--colors-gold-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget [text~="hocus:gold"]:hover,#feedback-widget [text~="hocus:gold"]:focus-visible{color:color-mix(in oklab,var(--colors-gold-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget [text~=neutral-300]{color:color-mix(in oklab,var(--colors-neutral-300) var(--un-text-opacity),transparent)}#feedback-widget [text~=neutral-700]{color:color-mix(in oklab,var(--colors-neutral-700) var(--un-text-opacity),transparent)}#feedback-widget [text~=neutral-800]{color:color-mix(in oklab,var(--colors-neutral-800) var(--un-text-opacity),transparent)}#feedback-widget [text~=neutral]{color:color-mix(in oklab,var(--colors-neutral-DEFAULT) var(--un-text-opacity),transparent)}#feedback-widget [text~=red-1100]{color:color-mix(in oklab,var(--colors-red-1100) var(--un-text-opacity),transparent)}#feedback-widget .outline-neutral-200{outline-color:color-mix(in oklab,var(--colors-neutral-200) var(--un-outline-opacity),transparent)}#feedback-widget [outline~=blue]{outline-color:color-mix(in oklab,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}#feedback-widget [outline~="neutral/15"]{outline-color:color-mix(in oklab,var(--colors-neutral-DEFAULT) 15%,transparent)}#feedback-widget [outline~=red-500]{outline-color:color-mix(in oklab,var(--colors-red-500) var(--un-outline-opacity),transparent)}#feedback-widget [outline~="white/8"]{outline-color:color-mix(in oklab,var(--colors-white) 8%,transparent)}#feedback-widget .focus-visible\:outline-blue:focus-visible{outline-color:color-mix(in oklab,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}#feedback-widget [focus-visible\:outline-blue=""]:focus-visible{outline-color:color-mix(in oklab,var(--colors-blue-DEFAULT) var(--un-outline-opacity),transparent)}#feedback-widget .bg-neutral-100{background-color:color-mix(in oklab,var(--colors-neutral-100) var(--un-bg-opacity),transparent)}#feedback-widget .bg-red-400{background-color:color-mix(in oklab,var(--colors-red-400) var(--un-bg-opacity),transparent)}#feedback-widget .bg-white{background-color:color-mix(in oklab,var(--colors-white) var(--un-bg-opacity),transparent)}#feedback-widget [bg-neutral-100=""]{background-color:color-mix(in oklab,var(--colors-neutral-100) var(--un-bg-opacity),transparent)}#feedback-widget [bg-red-400=""]{background-color:color-mix(in oklab,var(--colors-red-400) var(--un-bg-opacity),transparent)}#feedback-widget [bg-white=""]{background-color:color-mix(in oklab,var(--colors-white) var(--un-bg-opacity),transparent)}} diff --git a/public/widget.js b/public/widget.js index d1a53b4..b97384d 100644 --- a/public/widget.js +++ b/public/widget.js @@ -1,20 +1,20 @@ -(function(_t){typeof define=="function"&&define.amd?define(_t):_t()})(function(){"use strict";/** -* @vue/shared v3.5.17 +(function(_t){typeof define=="function"&&define.amd?define(_t):_t()})((function(){"use strict";/** +* @vue/shared v3.5.21 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**//*! #__NO_SIDE_EFFECTS__ */function _t(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const j={},vt=[],De=()=>{},Hi=()=>!1,rn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Nn=e=>e.startsWith("onUpdate:"),ie=Object.assign,Bn=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},ji=Object.prototype.hasOwnProperty,U=(e,t)=>ji.call(e,t),I=Array.isArray,yt=e=>Rt(e)==="[object Map]",on=e=>Rt(e)==="[object Set]",Fs=e=>Rt(e)==="[object Date]",D=e=>typeof e=="function",X=e=>typeof e=="string",Me=e=>typeof e=="symbol",K=e=>e!==null&&typeof e=="object",As=e=>(K(e)||D(e))&&D(e.then)&&D(e.catch),Ps=Object.prototype.toString,Rt=e=>Ps.call(e),Vi=e=>Rt(e).slice(8,-1),Is=e=>Rt(e)==="[object Object]",Hn=e=>X(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ot=_t(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),ln=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Ui=/-(\w)/g,Ee=ln(e=>e.replace(Ui,(t,n)=>n?n.toUpperCase():"")),qi=/\B([A-Z])/g,et=ln(e=>e.replace(qi,"-$1").toLowerCase()),cn=ln(e=>e.charAt(0).toUpperCase()+e.slice(1)),jn=ln(e=>e?`on${cn(e)}`:""),ye=(e,t)=>!Object.is(e,t),an=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Un=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Wi=e=>{const t=X(e)?Number(e):NaN;return isNaN(t)?e:t};let Ms;const un=()=>Ms||(Ms=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function xt(e){if(I(e)){const t={};for(let n=0;n{if(n){const s=n.split(zi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function ct(e){let t="";if(X(e))t=e;else if(I(e))for(let n=0;nwt(n,t))}const Os=e=>!!(e&&e.__v_isRef===!0),se=e=>X(e)?e:e==null?"":I(e)||K(e)&&(e.toString===Ps||!D(e.toString))?Os(e)?se(e.value):JSON.stringify(e,Ds,2):String(e),Ds=(e,t)=>Os(t)?Ds(e,t.value):yt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[qn(s,i)+" =>"]=r,n),{})}:on(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>qn(n))}:Me(t)?qn(t):K(t)&&!I(t)&&!Is(t)?String(t):t,qn=(e,t="")=>{var n;return Me(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** -* @vue/reactivity v3.5.17 +**/function _t(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const q={},yt=[],De=()=>{},Os=()=>!1,ln=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),jn=e=>e.startsWith("onUpdate:"),ie=Object.assign,Hn=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Ki=Object.prototype.hasOwnProperty,U=(e,t)=>Ki.call(e,t),I=Array.isArray,xt=e=>Dt(e)==="[object Map]",cn=e=>Dt(e)==="[object Set]",Rs=e=>Dt(e)==="[object Date]",R=e=>typeof e=="function",X=e=>typeof e=="string",Oe=e=>typeof e=="symbol",G=e=>e!==null&&typeof e=="object",$s=e=>(G(e)||R(e))&&R(e.then)&&R(e.catch),Ds=Object.prototype.toString,Dt=e=>Ds.call(e),zi=e=>Dt(e).slice(8,-1),Ls=e=>Dt(e)==="[object Object]",qn=e=>X(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Lt=_t(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),an=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Gi=/-\w/g,Fe=an(e=>e.replace(Gi,t=>t.slice(1).toUpperCase())),Ji=/\B([A-Z])/g,tt=an(e=>e.replace(Ji,"-$1").toLowerCase()),un=an(e=>e.charAt(0).toUpperCase()+e.slice(1)),Un=an(e=>e?`on${un(e)}`:""),ye=(e,t)=>!Object.is(e,t),fn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Wn=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Yi=e=>{const t=X(e)?Number(e):NaN;return isNaN(t)?e:t};let Bs;const dn=()=>Bs||(Bs=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function St(e){if(I(e)){const t={};for(let n=0;n{if(n){const s=n.split(Qi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function at(e){let t="";if(X(e))t=e;else if(I(e))for(let n=0;nwt(n,t))}const Hs=e=>!!(e&&e.__v_isRef===!0),ee=e=>X(e)?e:e==null?"":I(e)||G(e)&&(e.toString===Ds||!R(e.toString))?Hs(e)?ee(e.value):JSON.stringify(e,qs,2):String(e),qs=(e,t)=>Hs(t)?qs(e,t.value):xt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[Kn(s,i)+" =>"]=r,n),{})}:cn(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Kn(n))}:Oe(t)?Kn(t):G(t)&&!I(t)&&!Ls(t)?String(t):t,Kn=(e,t="")=>{var n;return Oe(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.21 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/let xe;class Qi{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=xe,!t&&xe&&(this.index=(xe.scopes||(xe.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0&&(xe=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if($t){let t=$t;for($t=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Dt;){let t=Dt;for(Dt=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function Bs(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Hs(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),Jn(s),eo(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function Gn(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(js(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function js(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Lt)||(e.globalVersion=Lt,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Gn(e))))return;e.flags|=2;const t=e.dep,n=G,s=ke;G=e,ke=!0;try{Bs(e);const r=e.fn(e._value);(t.version===0||ye(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{G=n,ke=s,Hs(e),e.flags&=-3}}function Jn(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)Jn(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function eo(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let ke=!0;const Vs=[];function $e(){Vs.push(ke),ke=!1}function Le(){const e=Vs.pop();ke=e===void 0?!0:e}function Us(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=G;G=void 0;try{t()}finally{G=n}}}let Lt=0;class to{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class fn{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!G||!ke||G===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==G)n=this.activeLink=new to(G,this),G.deps?(n.prevDep=G.depsTail,G.depsTail.nextDep=n,G.depsTail=n):G.deps=G.depsTail=n,qs(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=G.depsTail,n.nextDep=void 0,G.depsTail.nextDep=n,G.depsTail=n,G.deps===n&&(G.deps=s)}return n}trigger(t){this.version++,Lt++,this.notify(t)}notify(t){Kn();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{zn()}}}function qs(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)qs(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Yn=new WeakMap,at=Symbol(""),Zn=Symbol(""),Nt=Symbol("");function he(e,t,n){if(ke&&G){let s=Yn.get(e);s||Yn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new fn),r.map=s,r.key=n),r.track()}}function We(e,t,n,s,r,i){const o=Yn.get(e);if(!o){Lt++;return}const l=c=>{c&&c.trigger()};if(Kn(),t==="clear")o.forEach(l);else{const c=I(e),d=c&&Hn(n);if(c&&n==="length"){const u=Number(s);o.forEach((p,m)=>{(m==="length"||m===Nt||!Me(m)&&m>=u)&&l(p)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),d&&l(o.get(Nt)),t){case"add":c?d&&l(o.get("length")):(l(o.get(at)),yt(e)&&l(o.get(Zn)));break;case"delete":c||(l(o.get(at)),yt(e)&&l(o.get(Zn)));break;case"set":yt(e)&&l(o.get(at));break}}zn()}function St(e){const t=B(e);return t===e?t:(he(t,"iterate",Nt),Pe(e)?t:t.map(ue))}function dn(e){return he(e=B(e),"iterate",Nt),e}const no={__proto__:null,[Symbol.iterator](){return Qn(this,Symbol.iterator,ue)},concat(...e){return St(this).concat(...e.map(t=>I(t)?St(t):t))},entries(){return Qn(this,"entries",e=>(e[1]=ue(e[1]),e))},every(e,t){return Ke(this,"every",e,t,void 0,arguments)},filter(e,t){return Ke(this,"filter",e,t,n=>n.map(ue),arguments)},find(e,t){return Ke(this,"find",e,t,ue,arguments)},findIndex(e,t){return Ke(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Ke(this,"findLast",e,t,ue,arguments)},findLastIndex(e,t){return Ke(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Ke(this,"forEach",e,t,void 0,arguments)},includes(...e){return Xn(this,"includes",e)},indexOf(...e){return Xn(this,"indexOf",e)},join(e){return St(this).join(e)},lastIndexOf(...e){return Xn(this,"lastIndexOf",e)},map(e,t){return Ke(this,"map",e,t,void 0,arguments)},pop(){return Bt(this,"pop")},push(...e){return Bt(this,"push",e)},reduce(e,...t){return Ws(this,"reduce",e,t)},reduceRight(e,...t){return Ws(this,"reduceRight",e,t)},shift(){return Bt(this,"shift")},some(e,t){return Ke(this,"some",e,t,void 0,arguments)},splice(...e){return Bt(this,"splice",e)},toReversed(){return St(this).toReversed()},toSorted(e){return St(this).toSorted(e)},toSpliced(...e){return St(this).toSpliced(...e)},unshift(...e){return Bt(this,"unshift",e)},values(){return Qn(this,"values",ue)}};function Qn(e,t,n){const s=dn(e),r=s[t]();return s!==e&&!Pe(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.value&&(i.value=n(i.value)),i}),r}const so=Array.prototype;function Ke(e,t,n,s,r,i){const o=dn(e),l=o!==e&&!Pe(e),c=o[t];if(c!==so[t]){const p=c.apply(e,i);return l?ue(p):p}let d=n;o!==e&&(l?d=function(p,m){return n.call(this,ue(p),m,e)}:n.length>2&&(d=function(p,m){return n.call(this,p,m,e)}));const u=c.call(o,d,s);return l&&r?r(u):u}function Ws(e,t,n,s){const r=dn(e);let i=n;return r!==e&&(Pe(e)?n.length>3&&(i=function(o,l,c){return n.call(this,o,l,c,e)}):i=function(o,l,c){return n.call(this,o,ue(l),c,e)}),r[t](i,...s)}function Xn(e,t,n){const s=B(e);he(s,"iterate",Nt);const r=s[t](...n);return(r===-1||r===!1)&&ns(n[0])?(n[0]=B(n[0]),s[t](...n)):r}function Bt(e,t,n=[]){$e(),Kn();const s=B(e)[t].apply(e,n);return zn(),Le(),s}const ro=_t("__proto__,__v_isRef,__isVue"),Ks=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Me));function io(e){Me(e)||(e=String(e));const t=B(this);return he(t,"has",e),t.hasOwnProperty(e)}class zs{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?Xs:Qs:i?Zs:Ys).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=I(t);if(!r){let c;if(o&&(c=no[n]))return c;if(n==="hasOwnProperty")return io}const l=Reflect.get(t,n,fe(t)?t:s);return(Me(n)?Ks.has(n):ro(n))||(r||he(t,"get",n),i)?l:fe(l)?o&&Hn(n)?l:l.value:K(l)?r?er(l):ts(l):l}}class Gs extends zs{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];if(!this._isShallow){const c=tt(i);if(!Pe(s)&&!tt(s)&&(i=B(i),s=B(s)),!I(t)&&fe(i)&&!fe(s))return c?!1:(i.value=s,!0)}const o=I(t)&&Hn(n)?Number(n)e,pn=e=>Reflect.getPrototypeOf(e);function uo(e,t,n){return function(...s){const r=this.__v_raw,i=B(r),o=yt(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,d=r[e](...s),u=n?es:t?bn:ue;return!t&&he(i,"iterate",c?Zn:at),{next(){const{value:p,done:m}=d.next();return m?{value:p,done:m}:{value:l?[u(p[0]),u(p[1])]:u(p),done:m}},[Symbol.iterator](){return this}}}}function hn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function fo(e,t){const n={get(r){const i=this.__v_raw,o=B(i),l=B(r);e||(ye(r,l)&&he(o,"get",r),he(o,"get",l));const{has:c}=pn(o),d=t?es:e?bn:ue;if(c.call(o,r))return d(i.get(r));if(c.call(o,l))return d(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&he(B(r),"iterate",at),Reflect.get(r,"size",r)},has(r){const i=this.__v_raw,o=B(i),l=B(r);return e||(ye(r,l)&&he(o,"has",r),he(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=B(l),d=t?es:e?bn:ue;return!e&&he(c,"iterate",at),l.forEach((u,p)=>r.call(i,d(u),d(p),o))}};return ie(n,e?{add:hn("add"),set:hn("set"),delete:hn("delete"),clear:hn("clear")}:{add(r){!t&&!Pe(r)&&!tt(r)&&(r=B(r));const i=B(this);return pn(i).has.call(i,r)||(i.add(r),We(i,"add",r,r)),this},set(r,i){!t&&!Pe(i)&&!tt(i)&&(i=B(i));const o=B(this),{has:l,get:c}=pn(o);let d=l.call(o,r);d||(r=B(r),d=l.call(o,r));const u=c.call(o,r);return o.set(r,i),d?ye(i,u)&&We(o,"set",r,i):We(o,"add",r,i),this},delete(r){const i=B(this),{has:o,get:l}=pn(i);let c=o.call(i,r);c||(r=B(r),c=o.call(i,r)),l&&l.call(i,r);const d=i.delete(r);return c&&We(i,"delete",r,void 0),d},clear(){const r=B(this),i=r.size!==0,o=r.clear();return i&&We(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=uo(r,e,t)}),n}function mn(e,t){const n=fo(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(U(n,r)&&r in s?n:s,r,i)}const po={get:mn(!1,!1)},ho={get:mn(!1,!0)},mo={get:mn(!0,!1)},go={get:mn(!0,!0)},Ys=new WeakMap,Zs=new WeakMap,Qs=new WeakMap,Xs=new WeakMap;function bo(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function _o(e){return e.__v_skip||!Object.isExtensible(e)?0:bo(Vi(e))}function ts(e){return tt(e)?e:gn(e,!1,oo,po,Ys)}function vo(e){return gn(e,!1,co,ho,Zs)}function er(e){return gn(e,!0,lo,mo,Qs)}function Ta(e){return gn(e,!0,ao,go,Xs)}function gn(e,t,n,s,r){if(!K(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=_o(e);if(i===0)return e;const o=r.get(e);if(o)return o;const l=new Proxy(e,i===2?s:n);return r.set(e,l),l}function Ct(e){return tt(e)?Ct(e.__v_raw):!!(e&&e.__v_isReactive)}function tt(e){return!!(e&&e.__v_isReadonly)}function Pe(e){return!!(e&&e.__v_isShallow)}function ns(e){return e?!!e.__v_raw:!1}function B(e){const t=e&&e.__v_raw;return t?B(t):e}function yo(e){return!U(e,"__v_skip")&&Object.isExtensible(e)&&Vn(e,"__v_skip",!0),e}const ue=e=>K(e)?ts(e):e,bn=e=>K(e)?er(e):e;function fe(e){return e?e.__v_isRef===!0:!1}function Re(e){return xo(e,!1)}function xo(e,t){return fe(e)?e:new wo(e,t)}class wo{constructor(t,n){this.dep=new fn,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:B(t),this._value=n?t:ue(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Pe(t)||tt(t);t=s?t:B(t),ye(t,n)&&(this._rawValue=t,this._value=s?t:ue(t),this.dep.trigger())}}function H(e){return fe(e)?e.value:e}const So={get:(e,t,n)=>t==="__v_raw"?e:H(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return fe(r)&&!fe(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function tr(e){return Ct(e)?e:new Proxy(e,So)}class Co{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new fn,{get:s,set:r}=t(n.track.bind(n),n.trigger.bind(n));this._get=s,this._set=r}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Eo(e){return new Co(e)}class To{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new fn(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Lt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&G!==this)return Ns(this,!0),!0}get value(){const t=this.dep.track();return js(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Fo(e,t,n=!1){let s,r;return D(e)?s=e:(s=e.get,r=e.set),new To(s,r,n)}const _n={},vn=new WeakMap;let ut;function Ao(e,t=!1,n=ut){if(n){let s=vn.get(n);s||vn.set(n,s=[]),s.push(e)}}function Po(e,t,n=j){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,d=k=>r?k:Pe(k)||r===!1||r===0?ze(k,1):ze(k);let u,p,m,b,E=!1,T=!1;if(fe(e)?(p=()=>e.value,E=Pe(e)):Ct(e)?(p=()=>d(e),E=!0):I(e)?(T=!0,E=e.some(k=>Ct(k)||Pe(k)),p=()=>e.map(k=>{if(fe(k))return k.value;if(Ct(k))return d(k);if(D(k))return c?c(k,2):k()})):D(e)?t?p=c?()=>c(e,2):e:p=()=>{if(m){$e();try{m()}finally{Le()}}const k=ut;ut=u;try{return c?c(e,3,[b]):e(b)}finally{ut=k}}:p=De,t&&r){const k=p,J=r===!0?1/0:r;p=()=>ze(k(),J)}const R=Xi(),O=()=>{u.stop(),R&&R.active&&Bn(R.effects,u)};if(i&&t){const k=t;t=(...J)=>{k(...J),O()}}let V=T?new Array(e.length).fill(_n):_n;const z=k=>{if(!(!(u.flags&1)||!u.dirty&&!k))if(t){const J=u.run();if(r||E||(T?J.some((ce,ve)=>ye(ce,V[ve])):ye(J,V))){m&&m();const ce=ut;ut=u;try{const ve=[J,V===_n?void 0:T&&V[0]===_n?[]:V,b];V=J,c?c(t,3,ve):t(...ve)}finally{ut=ce}}}else u.run()};return l&&l(z),u=new $s(p),u.scheduler=o?()=>o(z,!1):z,b=k=>Ao(k,!1,u),m=u.onStop=()=>{const k=vn.get(u);if(k){if(c)c(k,4);else for(const J of k)J();vn.delete(u)}},t?s?z(!0):V=u.run():o?o(z.bind(null,!0),!0):u.run(),O.pause=u.pause.bind(u),O.resume=u.resume.bind(u),O.stop=O,O}function ze(e,t=1/0,n){if(t<=0||!K(e)||e.__v_skip||(n=n||new Set,n.has(e)))return e;if(n.add(e),t--,fe(e))ze(e.value,t,n);else if(I(e))for(let s=0;s{ze(s,t,n)});else if(Is(e)){for(const s in e)ze(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&ze(e[s],t,n)}return e}/** -* @vue/runtime-core v3.5.17 +**/let xe;class so{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=xe,!t&&xe&&(this.index=(xe.scopes||(xe.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0&&(xe=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(Bt){let t=Bt;for(Bt=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Nt;){let t=Nt;for(Nt=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function zs(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Gs(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),Zn(s),io(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function Yn(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Js(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Js(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Vt)||(e.globalVersion=Vt,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Yn(e))))return;e.flags|=2;const t=e.dep,n=J,s=Re;J=e,Re=!0;try{zs(e);const r=e.fn(e._value);(t.version===0||ye(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{J=n,Re=s,Gs(e),e.flags&=-3}}function Zn(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)Zn(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function io(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Re=!0;const Ys=[];function Le(){Ys.push(Re),Re=!1}function Ne(){const e=Ys.pop();Re=e===void 0?!0:e}function Zs(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=J;J=void 0;try{t()}finally{J=n}}}let Vt=0;class oo{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class pn{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!J||!Re||J===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==J)n=this.activeLink=new oo(J,this),J.deps?(n.prevDep=J.depsTail,J.depsTail.nextDep=n,J.depsTail=n):J.deps=J.depsTail=n,Qs(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=J.depsTail,n.nextDep=void 0,J.depsTail.nextDep=n,J.depsTail=n,J.deps===n&&(J.deps=s)}return n}trigger(t){this.version++,Vt++,this.notify(t)}notify(t){Gn();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Jn()}}}function Qs(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)Qs(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Qn=new WeakMap,ut=Symbol(""),Xn=Symbol(""),jt=Symbol("");function de(e,t,n){if(Re&&J){let s=Qn.get(e);s||Qn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new pn),r.map=s,r.key=n),r.track()}}function Ke(e,t,n,s,r,i){const o=Qn.get(e);if(!o){Vt++;return}const l=c=>{c&&c.trigger()};if(Gn(),t==="clear")o.forEach(l);else{const c=I(e),d=c&&qn(n);if(c&&n==="length"){const u=Number(s);o.forEach((p,m)=>{(m==="length"||m===jt||!Oe(m)&&m>=u)&&l(p)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),d&&l(o.get(jt)),t){case"add":c?d&&l(o.get("length")):(l(o.get(ut)),xt(e)&&l(o.get(Xn)));break;case"delete":c||(l(o.get(ut)),xt(e)&&l(o.get(Xn)));break;case"set":xt(e)&&l(o.get(ut));break}}Jn()}function Ct(e){const t=j(e);return t===e?t:(de(t,"iterate",jt),Ie(e)?t:t.map(ae))}function hn(e){return de(e=j(e),"iterate",jt),e}const lo={__proto__:null,[Symbol.iterator](){return es(this,Symbol.iterator,ae)},concat(...e){return Ct(this).concat(...e.map(t=>I(t)?Ct(t):t))},entries(){return es(this,"entries",e=>(e[1]=ae(e[1]),e))},every(e,t){return ze(this,"every",e,t,void 0,arguments)},filter(e,t){return ze(this,"filter",e,t,n=>n.map(ae),arguments)},find(e,t){return ze(this,"find",e,t,ae,arguments)},findIndex(e,t){return ze(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return ze(this,"findLast",e,t,ae,arguments)},findLastIndex(e,t){return ze(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return ze(this,"forEach",e,t,void 0,arguments)},includes(...e){return ts(this,"includes",e)},indexOf(...e){return ts(this,"indexOf",e)},join(e){return Ct(this).join(e)},lastIndexOf(...e){return ts(this,"lastIndexOf",e)},map(e,t){return ze(this,"map",e,t,void 0,arguments)},pop(){return Ht(this,"pop")},push(...e){return Ht(this,"push",e)},reduce(e,...t){return Xs(this,"reduce",e,t)},reduceRight(e,...t){return Xs(this,"reduceRight",e,t)},shift(){return Ht(this,"shift")},some(e,t){return ze(this,"some",e,t,void 0,arguments)},splice(...e){return Ht(this,"splice",e)},toReversed(){return Ct(this).toReversed()},toSorted(e){return Ct(this).toSorted(e)},toSpliced(...e){return Ct(this).toSpliced(...e)},unshift(...e){return Ht(this,"unshift",e)},values(){return es(this,"values",ae)}};function es(e,t,n){const s=hn(e),r=s[t]();return s!==e&&!Ie(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.value&&(i.value=n(i.value)),i}),r}const co=Array.prototype;function ze(e,t,n,s,r,i){const o=hn(e),l=o!==e&&!Ie(e),c=o[t];if(c!==co[t]){const p=c.apply(e,i);return l?ae(p):p}let d=n;o!==e&&(l?d=function(p,m){return n.call(this,ae(p),m,e)}:n.length>2&&(d=function(p,m){return n.call(this,p,m,e)}));const u=c.call(o,d,s);return l&&r?r(u):u}function Xs(e,t,n,s){const r=hn(e);let i=n;return r!==e&&(Ie(e)?n.length>3&&(i=function(o,l,c){return n.call(this,o,l,c,e)}):i=function(o,l,c){return n.call(this,o,ae(l),c,e)}),r[t](i,...s)}function ts(e,t,n){const s=j(e);de(s,"iterate",jt);const r=s[t](...n);return(r===-1||r===!1)&&rs(n[0])?(n[0]=j(n[0]),s[t](...n)):r}function Ht(e,t,n=[]){Le(),Gn();const s=j(e)[t].apply(e,n);return Jn(),Ne(),s}const ao=_t("__proto__,__v_isRef,__isVue"),er=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Oe));function uo(e){Oe(e)||(e=String(e));const t=j(this);return de(t,"has",e),t.hasOwnProperty(e)}class tr{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?lr:or:i?ir:rr).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=I(t);if(!r){let c;if(o&&(c=lo[n]))return c;if(n==="hasOwnProperty")return uo}const l=Reflect.get(t,n,ue(t)?t:s);return(Oe(n)?er.has(n):ao(n))||(r||de(t,"get",n),i)?l:ue(l)?o&&qn(n)?l:l.value:G(l)?r?cr(l):ss(l):l}}class nr extends tr{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];if(!this._isShallow){const c=nt(i);if(!Ie(s)&&!nt(s)&&(i=j(i),s=j(s)),!I(t)&&ue(i)&&!ue(s))return c||(i.value=s),!0}const o=I(t)&&qn(n)?Number(n)e,mn=e=>Reflect.getPrototypeOf(e);function go(e,t,n){return function(...s){const r=this.__v_raw,i=j(r),o=xt(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,d=r[e](...s),u=n?ns:t?_n:ae;return!t&&de(i,"iterate",c?Xn:ut),{next(){const{value:p,done:m}=d.next();return m?{value:p,done:m}:{value:l?[u(p[0]),u(p[1])]:u(p),done:m}},[Symbol.iterator](){return this}}}}function gn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function bo(e,t){const n={get(r){const i=this.__v_raw,o=j(i),l=j(r);e||(ye(r,l)&&de(o,"get",r),de(o,"get",l));const{has:c}=mn(o),d=t?ns:e?_n:ae;if(c.call(o,r))return d(i.get(r));if(c.call(o,l))return d(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&de(j(r),"iterate",ut),r.size},has(r){const i=this.__v_raw,o=j(i),l=j(r);return e||(ye(r,l)&&de(o,"has",r),de(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=j(l),d=t?ns:e?_n:ae;return!e&&de(c,"iterate",ut),l.forEach((u,p)=>r.call(i,d(u),d(p),o))}};return ie(n,e?{add:gn("add"),set:gn("set"),delete:gn("delete"),clear:gn("clear")}:{add(r){!t&&!Ie(r)&&!nt(r)&&(r=j(r));const i=j(this);return mn(i).has.call(i,r)||(i.add(r),Ke(i,"add",r,r)),this},set(r,i){!t&&!Ie(i)&&!nt(i)&&(i=j(i));const o=j(this),{has:l,get:c}=mn(o);let d=l.call(o,r);d||(r=j(r),d=l.call(o,r));const u=c.call(o,r);return o.set(r,i),d?ye(i,u)&&Ke(o,"set",r,i):Ke(o,"add",r,i),this},delete(r){const i=j(this),{has:o,get:l}=mn(i);let c=o.call(i,r);c||(r=j(r),c=o.call(i,r)),l&&l.call(i,r);const d=i.delete(r);return c&&Ke(i,"delete",r,void 0),d},clear(){const r=j(this),i=r.size!==0,o=r.clear();return i&&Ke(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=go(r,e,t)}),n}function bn(e,t){const n=bo(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(U(n,r)&&r in s?n:s,r,i)}const vo={get:bn(!1,!1)},_o={get:bn(!1,!0)},yo={get:bn(!0,!1)},xo={get:bn(!0,!0)},rr=new WeakMap,ir=new WeakMap,or=new WeakMap,lr=new WeakMap;function So(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function wo(e){return e.__v_skip||!Object.isExtensible(e)?0:So(zi(e))}function ss(e){return nt(e)?e:vn(e,!1,fo,vo,rr)}function Co(e){return vn(e,!1,ho,_o,ir)}function cr(e){return vn(e,!0,po,yo,or)}function Ra(e){return vn(e,!0,mo,xo,lr)}function vn(e,t,n,s,r){if(!G(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=wo(e);if(i===0)return e;const o=r.get(e);if(o)return o;const l=new Proxy(e,i===2?s:n);return r.set(e,l),l}function Tt(e){return nt(e)?Tt(e.__v_raw):!!(e&&e.__v_isReactive)}function nt(e){return!!(e&&e.__v_isReadonly)}function Ie(e){return!!(e&&e.__v_isShallow)}function rs(e){return e?!!e.__v_raw:!1}function j(e){const t=e&&e.__v_raw;return t?j(t):e}function To(e){return!U(e,"__v_skip")&&Object.isExtensible(e)&&Ns(e,"__v_skip",!0),e}const ae=e=>G(e)?ss(e):e,_n=e=>G(e)?cr(e):e;function ue(e){return e?e.__v_isRef===!0:!1}function ge(e){return Fo(e,!1)}function Fo(e,t){return ue(e)?e:new Eo(e,t)}class Eo{constructor(t,n){this.dep=new pn,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:j(t),this._value=n?t:ae(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Ie(t)||nt(t);t=s?t:j(t),ye(t,n)&&(this._rawValue=t,this._value=s?t:ae(t),this.dep.trigger())}}function H(e){return ue(e)?e.value:e}const Ao={get:(e,t,n)=>t==="__v_raw"?e:H(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return ue(r)&&!ue(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function ar(e){return Tt(e)?e:new Proxy(e,Ao)}class Po{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new pn,{get:s,set:r}=t(n.track.bind(n),n.trigger.bind(n));this._get=s,this._set=r}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Io(e){return new Po(e)}class ko{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new pn(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Vt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&J!==this)return Ks(this,!0),!0}get value(){const t=this.dep.track();return Js(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Mo(e,t,n=!1){let s,r;return R(e)?s=e:(s=e.get,r=e.set),new ko(s,r,n)}const yn={},xn=new WeakMap;let ft;function Oo(e,t=!1,n=ft){if(n){let s=xn.get(n);s||xn.set(n,s=[]),s.push(e)}}function Ro(e,t,n=q){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,d=P=>r?P:Ie(P)||r===!1||r===0?Ge(P,1):Ge(P);let u,p,m,v,A=!1,F=!1;if(ue(e)?(p=()=>e.value,A=Ie(e)):Tt(e)?(p=()=>d(e),A=!0):I(e)?(F=!0,A=e.some(P=>Tt(P)||Ie(P)),p=()=>e.map(P=>{if(ue(P))return P.value;if(Tt(P))return d(P);if(R(P))return c?c(P,2):P()})):R(e)?t?p=c?()=>c(e,2):e:p=()=>{if(m){Le();try{m()}finally{Ne()}}const P=ft;ft=u;try{return c?c(e,3,[v]):e(v)}finally{ft=P}}:p=De,t&&r){const P=p,W=r===!0?1/0:r;p=()=>Ge(P(),W)}const B=ro(),k=()=>{u.stop(),B&&B.active&&Hn(B.effects,u)};if(i&&t){const P=t;t=(...W)=>{P(...W),k()}}let L=F?new Array(e.length).fill(yn):yn;const D=P=>{if(!(!(u.flags&1)||!u.dirty&&!P))if(t){const W=u.run();if(r||A||(F?W.some((le,_e)=>ye(le,L[_e])):ye(W,L))){m&&m();const le=ft;ft=u;try{const _e=[W,L===yn?void 0:F&&L[0]===yn?[]:L,v];L=W,c?c(t,3,_e):t(..._e)}finally{ft=le}}}else u.run()};return l&&l(D),u=new Us(p),u.scheduler=o?()=>o(D,!1):D,v=P=>Oo(P,!1,u),m=u.onStop=()=>{const P=xn.get(u);if(P){if(c)c(P,4);else for(const W of P)W();xn.delete(u)}},t?s?D(!0):L=u.run():o?o(D.bind(null,!0),!0):u.run(),k.pause=u.pause.bind(u),k.resume=u.resume.bind(u),k.stop=k,k}function Ge(e,t=1/0,n){if(t<=0||!G(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,ue(e))Ge(e.value,t,n);else if(I(e))for(let s=0;s{Ge(s,t,n)});else if(Ls(e)){for(const s in e)Ge(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&Ge(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.21 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const Ht=[];let ss=!1;function Fa(e,...t){if(ss)return;ss=!0,$e();const n=Ht.length?Ht[Ht.length-1].component:null,s=n&&n.appContext.config.warnHandler,r=Io();if(s)Et(s,n,11,[e+t.map(i=>{var o,l;return(l=(o=i.toString)==null?void 0:o.call(i))!=null?l:JSON.stringify(i)}).join(""),n&&n.proxy,r.map(({vnode:i})=>`at <${ii(n,i.type)}>`).join(` +**/const qt=[];let is=!1;function $a(e,...t){if(is)return;is=!0,Le();const n=qt.length?qt[qt.length-1].component:null,s=n&&n.appContext.config.warnHandler,r=$o();if(s)Ft(s,n,11,[e+t.map(i=>{var o,l;return(l=(o=i.toString)==null?void 0:o.call(i))!=null?l:JSON.stringify(i)}).join(""),n&&n.proxy,r.map(({vnode:i})=>`at <${fi(n,i.type)}>`).join(` `),r]);else{const i=[`[Vue warn]: ${e}`,...t];r.length&&i.push(` -`,...Mo(r)),console.warn(...i)}Le(),ss=!1}function Io(){let e=Ht[Ht.length-1];if(!e)return[];const t=[];for(;e;){const n=t[0];n&&n.vnode===e?n.recurseCount++:t.push({vnode:e,recurseCount:0});const s=e.component&&e.component.parent;e=s&&s.vnode}return t}function Mo(e){const t=[];return e.forEach((n,s)=>{t.push(...s===0?[]:[` -`],...ko(n))}),t}function ko({vnode:e,recurseCount:t}){const n=t>0?`... (${t} recursive calls)`:"",s=e.component?e.component.parent==null:!1,r=` at <${ii(e.component,e.type,s)}`,i=">"+n;return e.props?[r,...Ro(e.props),i]:[r+i]}function Ro(e){const t=[],n=Object.keys(e);return n.slice(0,3).forEach(s=>{t.push(...nr(s,e[s]))}),n.length>3&&t.push(" ..."),t}function nr(e,t,n){return X(t)?(t=JSON.stringify(t),n?t:[`${e}=${t}`]):typeof t=="number"||typeof t=="boolean"||t==null?n?t:[`${e}=${t}`]:fe(t)?(t=nr(e,B(t.value),!0),n?t:[`${e}=Ref<`,t,">"]):D(t)?[`${e}=fn${t.name?`<${t.name}>`:""}`]:(t=B(t),n?t:[`${e}=`,t])}function Et(e,t,n,s){try{return s?e(...s):e()}catch(r){yn(r,t,n)}}function Oe(e,t,n,s){if(D(e)){const r=Et(e,t,n,s);return r&&As(r)&&r.catch(i=>{yn(i,t,n)}),r}if(I(e)){const r=[];for(let i=0;i>>1,r=be[s],i=jt(r);i=jt(n)?be.push(e):be.splice($o(t),0,e),e.flags|=1,rr()}}function rr(){xn||(xn=sr.then(lr))}function Lo(e){I(e)?Tt.push(...e):nt&&e.id===-1?nt.splice(Ft+1,0,e):e.flags&1||(Tt.push(e),e.flags|=1),rr()}function ir(e,t,n=Ne+1){for(;njt(n)-jt(s));if(Tt.length=0,nt){nt.push(...t);return}for(nt=t,Ft=0;Fte.id==null?e.flags&2?-1:1/0:e.id;function lr(e){try{for(Ne=0;Ne{s._d&&Yr(-1);const i=wn(t);let o;try{o=e(...r)}finally{wn(i),s._d&&Yr(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function Sn(e,t){if(oe===null)return e;const n=Rn(oe),s=e.dirs||(e.dirs=[]);for(let r=0;re.__isTeleport,st=Symbol("_leaveCb"),Cn=Symbol("_enterCb");function Bo(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return _r(()=>{e.isMounted=!0}),vr(()=>{e.isUnmounting=!0}),e}const Ie=[Function,Array],ur={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Ie,onEnter:Ie,onAfterEnter:Ie,onEnterCancelled:Ie,onBeforeLeave:Ie,onLeave:Ie,onAfterLeave:Ie,onLeaveCancelled:Ie,onBeforeAppear:Ie,onAppear:Ie,onAfterAppear:Ie,onAppearCancelled:Ie},fr=e=>{const t=e.subTree;return t.component?fr(t.component):t},Ho={name:"BaseTransition",props:ur,setup(e,{slots:t}){const n=Xr(),s=Bo();return()=>{const r=t.default&&mr(t.default(),!0);if(!r||!r.length)return;const i=dr(r),o=B(e),{mode:l}=o;if(s.isLeaving)return ls(i);const c=hr(i);if(!c)return ls(i);let d=os(c,o,s,n,p=>d=p);c.type!==me&&Vt(c,d);let u=n.subTree&&hr(n.subTree);if(u&&u.type!==me&&!pt(c,u)&&fr(n).type!==me){let p=os(u,o,s,n);if(Vt(u,p),l==="out-in"&&c.type!==me)return s.isLeaving=!0,p.afterLeave=()=>{s.isLeaving=!1,n.job.flags&8||n.update(),delete p.afterLeave,u=void 0},ls(i);l==="in-out"&&c.type!==me?p.delayLeave=(m,b,E)=>{const T=pr(s,u);T[String(u.key)]=u,m[st]=()=>{b(),m[st]=void 0,delete d.delayedLeave,u=void 0},d.delayedLeave=()=>{E(),delete d.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return i}}};function dr(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==me){t=n;break}}return t}const jo=Ho;function pr(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function os(e,t,n,s,r){const{appear:i,mode:o,persisted:l=!1,onBeforeEnter:c,onEnter:d,onAfterEnter:u,onEnterCancelled:p,onBeforeLeave:m,onLeave:b,onAfterLeave:E,onLeaveCancelled:T,onBeforeAppear:R,onAppear:O,onAfterAppear:V,onAppearCancelled:z}=t,k=String(e.key),J=pr(n,e),ce=(L,W)=>{L&&Oe(L,s,9,W)},ve=(L,W)=>{const te=W[1];ce(L,W),I(L)?L.every(A=>A.length<=1)&&te():L.length<=1&&te()},we={mode:o,persisted:l,beforeEnter(L){let W=c;if(!n.isMounted)if(i)W=R||c;else return;L[st]&&L[st](!0);const te=J[k];te&&pt(e,te)&&te.el[st]&&te.el[st](),ce(W,[L])},enter(L){let W=d,te=u,A=p;if(!n.isMounted)if(i)W=O||d,te=V||u,A=z||p;else return;let Q=!1;const ge=L[Cn]=Xe=>{Q||(Q=!0,Xe?ce(A,[L]):ce(te,[L]),we.delayedLeave&&we.delayedLeave(),L[Cn]=void 0)};W?ve(W,[L,ge]):ge()},leave(L,W){const te=String(e.key);if(L[Cn]&&L[Cn](!0),n.isUnmounting)return W();ce(m,[L]);let A=!1;const Q=L[st]=ge=>{A||(A=!0,W(),ge?ce(T,[L]):ce(E,[L]),L[st]=void 0,J[te]===e&&delete J[te])};J[te]=e,b?ve(b,[L,Q]):Q()},clone(L){const W=os(L,t,n,s,r);return r&&r(W),W}};return we}function ls(e){if(En(e))return e=rt(e),e.children=null,e}function hr(e){if(!En(e))return ar(e.type)&&e.children?dr(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&D(n.default))return n.default()}}function Vt(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Vt(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function mr(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;iUt(E,t&&(I(t)?t[T]:t),n,s,r));return}if(Pt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Ut(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?Rn(s.component):s.el,o=r?null:i,{i:l,r:c}=e,d=t&&t.r,u=l.refs===j?l.refs={}:l.refs,p=l.setupState,m=B(p),b=p===j?()=>!1:E=>U(m,E);if(d!=null&&d!==c&&(X(d)?(u[d]=null,b(d)&&(p[d]=null)):fe(d)&&(d.value=null)),D(c))Et(c,l,12,[o,u]);else{const E=X(c),T=fe(c);if(E||T){const R=()=>{if(e.f){const O=E?b(c)?p[c]:u[c]:c.value;r?I(O)&&Bn(O,i):I(O)?O.includes(i)||O.push(i):E?(u[c]=[i],b(c)&&(p[c]=u[c])):(c.value=[i],e.k&&(u[e.k]=c.value))}else E?(u[c]=o,b(c)&&(p[c]=o)):T&&(c.value=o,e.k&&(u[e.k]=o))};o?(R.id=-1,Te(R,n)):R()}}}un().requestIdleCallback,un().cancelIdleCallback;const Pt=e=>!!e.type.__asyncLoader,En=e=>e.type.__isKeepAlive;function Vo(e,t){br(e,"a",t)}function Uo(e,t){br(e,"da",t)}function br(e,t,n=pe){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Tn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)En(r.parent.vnode)&&qo(s,t,n,r),r=r.parent}}function qo(e,t,n,s){const r=Tn(t,e,s,!0);yr(()=>{Bn(s[t],r)},n)}function Tn(e,t,n=pe,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{$e();const l=Zt(n),c=Oe(t,n,e,o);return l(),Le(),c});return s?r.unshift(i):r.push(i),i}}const Ge=e=>(t,n=pe)=>{(!Qt||e==="sp")&&Tn(e,(...s)=>t(...s),n)},Wo=Ge("bm"),_r=Ge("m"),Ko=Ge("bu"),zo=Ge("u"),vr=Ge("bum"),yr=Ge("um"),Go=Ge("sp"),Jo=Ge("rtg"),Yo=Ge("rtc");function Zo(e,t=pe){Tn("ec",e,t)}const Qo="components",xr=Symbol.for("v-ndc");function Xo(e){return X(e)?el(Qo,e,!1)||e:e||xr}function el(e,t,n=!0,s=!1){const r=oe||pe;if(r){const i=r.type;{const l=ri(i,!1);if(l&&(l===t||l===Ee(t)||l===cn(Ee(t))))return i}const o=wr(r[e]||i[e],t)||wr(r.appContext[e],t);return!o&&s?i:o}}function wr(e,t){return e&&(e[t]||e[Ee(t)]||e[cn(Ee(t))])}function cs(e,t,n,s){let r;const i=n,o=I(e);if(o||X(e)){const l=o&&Ct(e);let c=!1,d=!1;l&&(c=!Pe(e),d=tt(e),e=dn(e)),r=new Array(e.length);for(let u=0,p=e.length;ut(l,c,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,d=l.length;cJt(t)?!(t.type===me||t.type===re&&!Sr(t.children)):!0)?e:null}const as=e=>e?ti(e)?Rn(e):as(e.parent):null,qt=ie(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>as(e.parent),$root:e=>as(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Fr(e),$forceUpdate:e=>e.f||(e.f=()=>{rs(e.update)}),$nextTick:e=>e.n||(e.n=Do.bind(e.proxy)),$watch:e=>Sl.bind(e)}),us=(e,t)=>e!==j&&!e.__isScriptSetup&&U(e,t),nl={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;let d;if(t[0]!=="$"){const b=o[t];if(b!==void 0)switch(b){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(us(s,t))return o[t]=1,s[t];if(r!==j&&U(r,t))return o[t]=2,r[t];if((d=e.propsOptions[0])&&U(d,t))return o[t]=3,i[t];if(n!==j&&U(n,t))return o[t]=4,n[t];fs&&(o[t]=0)}}const u=qt[t];let p,m;if(u)return t==="$attrs"&&he(e.attrs,"get",""),u(e);if((p=l.__cssModules)&&(p=p[t]))return p;if(n!==j&&U(n,t))return o[t]=4,n[t];if(m=c.config.globalProperties,U(m,t))return m[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return us(r,t)?(r[t]=n,!0):s!==j&&U(s,t)?(s[t]=n,!0):U(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i}},o){let l;return!!n[o]||e!==j&&U(e,o)||us(t,o)||(l=i[0])&&U(l,o)||U(s,o)||U(qt,o)||U(r.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:U(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Cr(e){return I(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let fs=!0;function sl(e){const t=Fr(e),n=e.proxy,s=e.ctx;fs=!1,t.beforeCreate&&Er(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:d,created:u,beforeMount:p,mounted:m,beforeUpdate:b,updated:E,activated:T,deactivated:R,beforeDestroy:O,beforeUnmount:V,destroyed:z,unmounted:k,render:J,renderTracked:ce,renderTriggered:ve,errorCaptured:we,serverPrefetch:L,expose:W,inheritAttrs:te,components:A,directives:Q,filters:ge}=t;if(d&&rl(d,s,null),o)for(const ne in o){const Y=o[ne];D(Y)&&(s[ne]=Y.bind(n))}if(r){const ne=r.call(n,n);K(ne)&&(e.data=ts(ne))}if(fs=!0,i)for(const ne in i){const Y=i[ne],gt=D(Y)?Y.bind(n,n):D(Y.get)?Y.get.bind(n,n):De,$n=!D(Y)&&D(Y.set)?Y.set.bind(n):De,bt=oi({get:gt,set:$n});Object.defineProperty(s,ne,{enumerable:!0,configurable:!0,get:()=>bt.value,set:je=>bt.value=je})}if(l)for(const ne in l)Tr(l[ne],s,n,ne);if(c){const ne=D(c)?c.call(n):c;Reflect.ownKeys(ne).forEach(Y=>{Mr(Y,ne[Y])})}u&&Er(u,e,"c");function ae(ne,Y){I(Y)?Y.forEach(gt=>ne(gt.bind(n))):Y&&ne(Y.bind(n))}if(ae(Wo,p),ae(_r,m),ae(Ko,b),ae(zo,E),ae(Vo,T),ae(Uo,R),ae(Zo,we),ae(Yo,ce),ae(Jo,ve),ae(vr,V),ae(yr,k),ae(Go,L),I(W))if(W.length){const ne=e.exposed||(e.exposed={});W.forEach(Y=>{Object.defineProperty(ne,Y,{get:()=>n[Y],set:gt=>n[Y]=gt})})}else e.exposed||(e.exposed={});J&&e.render===De&&(e.render=J),te!=null&&(e.inheritAttrs=te),A&&(e.components=A),Q&&(e.directives=Q),L&&gr(e)}function rl(e,t,n=De){I(e)&&(e=ds(e));for(const s in e){const r=e[s];let i;K(r)?"default"in r?i=Je(r.from||s,r.default,!0):i=Je(r.from||s):i=Je(r),fe(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function Er(e,t,n){Oe(I(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Tr(e,t,n,s){let r=s.includes(".")?qr(n,s):()=>n[s];if(X(e)){const i=t[e];D(i)&&An(r,i)}else if(D(e))An(r,e.bind(n));else if(K(e))if(I(e))e.forEach(i=>Tr(i,t,n,s));else{const i=D(e.handler)?e.handler.bind(n):t[e.handler];D(i)&&An(r,i,e)}}function Fr(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(d=>Fn(c,d,o,!0)),Fn(c,t,o)),K(t)&&i.set(t,c),c}function Fn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Fn(e,i,n,!0),r&&r.forEach(o=>Fn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=il[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const il={data:Ar,props:Pr,emits:Pr,methods:Wt,computed:Wt,beforeCreate:_e,created:_e,beforeMount:_e,mounted:_e,beforeUpdate:_e,updated:_e,beforeDestroy:_e,beforeUnmount:_e,destroyed:_e,unmounted:_e,activated:_e,deactivated:_e,errorCaptured:_e,serverPrefetch:_e,components:Wt,directives:Wt,watch:ll,provide:Ar,inject:ol};function Ar(e,t){return t?e?function(){return ie(D(e)?e.call(this,this):e,D(t)?t.call(this,this):t)}:t:e}function ol(e,t){return Wt(ds(e),ds(t))}function ds(e){if(I(e)){const t={};for(let n=0;n1)return n&&D(t)?t.call(s&&s.proxy):t}}const kr={},Rr=()=>Object.create(kr),Or=e=>Object.getPrototypeOf(e)===kr;function ul(e,t,n,s=!1){const r={},i=Rr();e.propsDefaults=Object.create(null),Dr(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:vo(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function fl(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=B(r),[c]=e.propsOptions;let d=!1;if((s||o>0)&&!(o&16)){if(o&8){const u=e.vnode.dynamicProps;for(let p=0;p{c=!0;const[m,b]=$r(p,t,!0);ie(o,m),b&&l.push(...b)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!i&&!c)return K(e)&&s.set(e,vt),vt;if(I(i))for(let u=0;ue[0]==="_"||e==="$stable",ms=e=>I(e)?e.map(Be):[Be(e)],pl=(e,t,n)=>{if(t._n)return t;const s=is((...r)=>ms(t(...r)),n);return s._c=!1,s},Nr=(e,t,n)=>{const s=e._ctx;for(const r in e){if(hs(r))continue;const i=e[r];if(D(i))t[r]=pl(r,i,s);else if(i!=null){const o=ms(i);t[r]=()=>o}}},Br=(e,t)=>{const n=ms(t);e.slots.default=()=>n},Hr=(e,t,n)=>{for(const s in t)(n||!hs(s))&&(e[s]=t[s])},hl=(e,t,n)=>{const s=e.slots=Rr();if(e.vnode.shapeFlag&32){const r=t.__;r&&Vn(s,"__",r,!0);const i=t._;i?(Hr(s,t,n),n&&Vn(s,"_",i,!0)):Nr(t,s)}else t&&Br(e,t)},ml=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=j;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:Hr(r,t,n):(i=!t.$stable,Nr(t,r)),o=t}else t&&(Br(e,t),o={default:1});if(i)for(const l in r)!hs(l)&&o[l]==null&&delete r[l]},Te=Il;function gl(e){return bl(e)}function bl(e,t){const n=un();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:d,setElementText:u,parentNode:p,nextSibling:m,setScopeId:b=De,insertStaticContent:E}=e,T=(a,f,h,v=null,g=null,_=null,S=void 0,w=null,x=!!f.dynamicChildren)=>{if(a===f)return;a&&!pt(a,f)&&(v=Ln(a),je(a,g,_,!0),a=null),f.patchFlag===-2&&(x=!1,f.dynamicChildren=null);const{type:y,ref:M,shapeFlag:C}=f;switch(y){case In:R(a,f,h,v);break;case me:O(a,f,h,v);break;case _s:a==null&&V(f,h,v,S);break;case re:A(a,f,h,v,g,_,S,w,x);break;default:C&1?J(a,f,h,v,g,_,S,w,x):C&6?Q(a,f,h,v,g,_,S,w,x):(C&64||C&128)&&y.process(a,f,h,v,g,_,S,w,x,nn)}M!=null&&g?Ut(M,a&&a.ref,_,f||a,!f):M==null&&a&&a.ref!=null&&Ut(a.ref,null,_,a,!0)},R=(a,f,h,v)=>{if(a==null)s(f.el=l(f.children),h,v);else{const g=f.el=a.el;f.children!==a.children&&d(g,f.children)}},O=(a,f,h,v)=>{a==null?s(f.el=c(f.children||""),h,v):f.el=a.el},V=(a,f,h,v)=>{[a.el,a.anchor]=E(a.children,f,h,v,a.el,a.anchor)},z=({el:a,anchor:f},h,v)=>{let g;for(;a&&a!==f;)g=m(a),s(a,h,v),a=g;s(f,h,v)},k=({el:a,anchor:f})=>{let h;for(;a&&a!==f;)h=m(a),r(a),a=h;r(f)},J=(a,f,h,v,g,_,S,w,x)=>{f.type==="svg"?S="svg":f.type==="math"&&(S="mathml"),a==null?ce(f,h,v,g,_,S,w,x):L(a,f,g,_,S,w,x)},ce=(a,f,h,v,g,_,S,w)=>{let x,y;const{props:M,shapeFlag:C,transition:P,dirs:$}=a;if(x=a.el=o(a.type,_,M&&M.is,M),C&8?u(x,a.children):C&16&&we(a.children,x,null,v,g,gs(a,_),S,w),$&&ft(a,null,v,"created"),ve(x,a,a.scopeId,S,v),M){for(const Z in M)Z!=="value"&&!Ot(Z)&&i(x,Z,null,M[Z],_,v);"value"in M&&i(x,"value",null,M.value,_),(y=M.onVnodeBeforeMount)&&He(y,v,a)}$&&ft(a,null,v,"beforeMount");const N=_l(g,P);N&&P.beforeEnter(x),s(x,f,h),((y=M&&M.onVnodeMounted)||N||$)&&Te(()=>{y&&He(y,v,a),N&&P.enter(x),$&&ft(a,null,v,"mounted")},g)},ve=(a,f,h,v,g)=>{if(h&&b(a,h),v)for(let _=0;_{for(let y=x;y{const w=f.el=a.el;let{patchFlag:x,dynamicChildren:y,dirs:M}=f;x|=a.patchFlag&16;const C=a.props||j,P=f.props||j;let $;if(h&&dt(h,!1),($=P.onVnodeBeforeUpdate)&&He($,h,f,a),M&&ft(f,a,h,"beforeUpdate"),h&&dt(h,!0),(C.innerHTML&&P.innerHTML==null||C.textContent&&P.textContent==null)&&u(w,""),y?W(a.dynamicChildren,y,w,h,v,gs(f,g),_):S||Y(a,f,w,null,h,v,gs(f,g),_,!1),x>0){if(x&16)te(w,C,P,h,g);else if(x&2&&C.class!==P.class&&i(w,"class",null,P.class,g),x&4&&i(w,"style",C.style,P.style,g),x&8){const N=f.dynamicProps;for(let Z=0;Z{$&&He($,h,f,a),M&&ft(f,a,h,"updated")},v)},W=(a,f,h,v,g,_,S)=>{for(let w=0;w{if(f!==h){if(f!==j)for(const _ in f)!Ot(_)&&!(_ in h)&&i(a,_,f[_],null,g,v);for(const _ in h){if(Ot(_))continue;const S=h[_],w=f[_];S!==w&&_!=="value"&&i(a,_,w,S,g,v)}"value"in h&&i(a,"value",f.value,h.value,g)}},A=(a,f,h,v,g,_,S,w,x)=>{const y=f.el=a?a.el:l(""),M=f.anchor=a?a.anchor:l("");let{patchFlag:C,dynamicChildren:P,slotScopeIds:$}=f;$&&(w=w?w.concat($):$),a==null?(s(y,h,v),s(M,h,v),we(f.children||[],h,M,g,_,S,w,x)):C>0&&C&64&&P&&a.dynamicChildren?(W(a.dynamicChildren,P,h,g,_,S,w),(f.key!=null||g&&f===g.subTree)&&jr(a,f,!0)):Y(a,f,h,M,g,_,S,w,x)},Q=(a,f,h,v,g,_,S,w,x)=>{f.slotScopeIds=w,a==null?f.shapeFlag&512?g.ctx.activate(f,h,v,S,x):ge(f,h,v,g,_,S,x):Xe(a,f,x)},ge=(a,f,h,v,g,_,S)=>{const w=a.component=Ll(a,v,g);if(En(a)&&(w.ctx.renderer=nn),Nl(w,!1,S),w.asyncDep){if(g&&g.registerDep(w,ae,S),!a.el){const x=w.subTree=de(me);O(null,x,f,h)}}else ae(w,a,f,h,g,_,S)},Xe=(a,f,h)=>{const v=f.component=a.component;if(Al(a,f,h))if(v.asyncDep&&!v.asyncResolved){ne(v,f,h);return}else v.next=f,v.update();else f.el=a.el,v.vnode=f},ae=(a,f,h,v,g,_,S)=>{const w=()=>{if(a.isMounted){let{next:C,bu:P,u:$,parent:N,vnode:Z}=a;{const Ue=Vr(a);if(Ue){C&&(C.el=Z.el,ne(a,C,S)),Ue.asyncDep.then(()=>{a.isUnmounted||w()});return}}let q=C,Se;dt(a,!1),C?(C.el=Z.el,ne(a,C,S)):C=Z,P&&an(P),(Se=C.props&&C.props.onVnodeBeforeUpdate)&&He(Se,N,C,Z),dt(a,!0);const Ce=zr(a),Ve=a.subTree;a.subTree=Ce,T(Ve,Ce,p(Ve.el),Ln(Ve),a,g,_),C.el=Ce.el,q===null&&Pl(a,Ce.el),$&&Te($,g),(Se=C.props&&C.props.onVnodeUpdated)&&Te(()=>He(Se,N,C,Z),g)}else{let C;const{el:P,props:$}=f,{bm:N,m:Z,parent:q,root:Se,type:Ce}=a,Ve=Pt(f);dt(a,!1),N&&an(N),!Ve&&(C=$&&$.onVnodeBeforeMount)&&He(C,q,f),dt(a,!0);{Se.ce&&Se.ce._def.shadowRoot!==!1&&Se.ce._injectChildStyle(Ce);const Ue=a.subTree=zr(a);T(null,Ue,h,v,a,g,_),f.el=Ue.el}if(Z&&Te(Z,g),!Ve&&(C=$&&$.onVnodeMounted)){const Ue=f;Te(()=>He(C,q,Ue),g)}(f.shapeFlag&256||q&&Pt(q.vnode)&&q.vnode.shapeFlag&256)&&a.a&&Te(a.a,g),a.isMounted=!0,f=h=v=null}};a.scope.on();const x=a.effect=new $s(w);a.scope.off();const y=a.update=x.run.bind(x),M=a.job=x.runIfDirty.bind(x);M.i=a,M.id=a.uid,x.scheduler=()=>rs(M),dt(a,!0),y()},ne=(a,f,h)=>{f.component=a;const v=a.vnode.props;a.vnode=f,a.next=null,fl(a,f.props,v,h),ml(a,f.children,h),$e(),ir(a),Le()},Y=(a,f,h,v,g,_,S,w,x=!1)=>{const y=a&&a.children,M=a?a.shapeFlag:0,C=f.children,{patchFlag:P,shapeFlag:$}=f;if(P>0){if(P&128){$n(y,C,h,v,g,_,S,w,x);return}else if(P&256){gt(y,C,h,v,g,_,S,w,x);return}}$&8?(M&16&&tn(y,g,_),C!==y&&u(h,C)):M&16?$&16?$n(y,C,h,v,g,_,S,w,x):tn(y,g,_,!0):(M&8&&u(h,""),$&16&&we(C,h,v,g,_,S,w,x))},gt=(a,f,h,v,g,_,S,w,x)=>{a=a||vt,f=f||vt;const y=a.length,M=f.length,C=Math.min(y,M);let P;for(P=0;PM?tn(a,g,_,!0,!1,C):we(f,h,v,g,_,S,w,x,C)},$n=(a,f,h,v,g,_,S,w,x)=>{let y=0;const M=f.length;let C=a.length-1,P=M-1;for(;y<=C&&y<=P;){const $=a[y],N=f[y]=x?it(f[y]):Be(f[y]);if(pt($,N))T($,N,h,null,g,_,S,w,x);else break;y++}for(;y<=C&&y<=P;){const $=a[C],N=f[P]=x?it(f[P]):Be(f[P]);if(pt($,N))T($,N,h,null,g,_,S,w,x);else break;C--,P--}if(y>C){if(y<=P){const $=P+1,N=$P)for(;y<=C;)je(a[y],g,_,!0),y++;else{const $=y,N=y,Z=new Map;for(y=N;y<=P;y++){const Ae=f[y]=x?it(f[y]):Be(f[y]);Ae.key!=null&&Z.set(Ae.key,y)}let q,Se=0;const Ce=P-N+1;let Ve=!1,Ue=0;const sn=new Array(Ce);for(y=0;y=Ce){je(Ae,g,_,!0);continue}let qe;if(Ae.key!=null)qe=Z.get(Ae.key);else for(q=N;q<=P;q++)if(sn[q-N]===0&&pt(Ae,f[q])){qe=q;break}qe===void 0?je(Ae,g,_,!0):(sn[qe-N]=y+1,qe>=Ue?Ue=qe:Ve=!0,T(Ae,f[qe],h,null,g,_,S,w,x),Se++)}const Ni=Ve?vl(sn):vt;for(q=Ni.length-1,y=Ce-1;y>=0;y--){const Ae=N+y,qe=f[Ae],Bi=Ae+1{const{el:_,type:S,transition:w,children:x,shapeFlag:y}=a;if(y&6){bt(a.component.subTree,f,h,v);return}if(y&128){a.suspense.move(f,h,v);return}if(y&64){S.move(a,f,h,nn);return}if(S===re){s(_,f,h);for(let C=0;Cw.enter(_),g);else{const{leave:C,delayLeave:P,afterLeave:$}=w,N=()=>{a.ctx.isUnmounted?r(_):s(_,f,h)},Z=()=>{C(_,()=>{N(),$&&$()})};P?P(_,N,Z):Z()}else s(_,f,h)},je=(a,f,h,v=!1,g=!1)=>{const{type:_,props:S,ref:w,children:x,dynamicChildren:y,shapeFlag:M,patchFlag:C,dirs:P,cacheIndex:$}=a;if(C===-2&&(g=!1),w!=null&&($e(),Ut(w,null,h,a,!0),Le()),$!=null&&(f.renderCache[$]=void 0),M&256){f.ctx.deactivate(a);return}const N=M&1&&P,Z=!Pt(a);let q;if(Z&&(q=S&&S.onVnodeBeforeUnmount)&&He(q,f,a),M&6)Ca(a.component,h,v);else{if(M&128){a.suspense.unmount(h,v);return}N&&ft(a,null,f,"beforeUnmount"),M&64?a.type.remove(a,f,h,nn,v):y&&!y.hasOnce&&(_!==re||C>0&&C&64)?tn(y,f,h,!1,!0):(_===re&&C&384||!g&&M&16)&&tn(x,f,h),v&&$i(a)}(Z&&(q=S&&S.onVnodeUnmounted)||N)&&Te(()=>{q&&He(q,f,a),N&&ft(a,null,f,"unmounted")},h)},$i=a=>{const{type:f,el:h,anchor:v,transition:g}=a;if(f===re){Sa(h,v);return}if(f===_s){k(a);return}const _=()=>{r(h),g&&!g.persisted&&g.afterLeave&&g.afterLeave()};if(a.shapeFlag&1&&g&&!g.persisted){const{leave:S,delayLeave:w}=g,x=()=>S(h,_);w?w(a.el,_,x):x()}else _()},Sa=(a,f)=>{let h;for(;a!==f;)h=m(a),r(a),a=h;r(f)},Ca=(a,f,h)=>{const{bum:v,scope:g,job:_,subTree:S,um:w,m:x,a:y,parent:M,slots:{__:C}}=a;Ur(x),Ur(y),v&&an(v),M&&I(C)&&C.forEach(P=>{M.renderCache[P]=void 0}),g.stop(),_&&(_.flags|=8,je(S,a,f,h)),w&&Te(w,f),Te(()=>{a.isUnmounted=!0},f),f&&f.pendingBranch&&!f.isUnmounted&&a.asyncDep&&!a.asyncResolved&&a.suspenseId===f.pendingId&&(f.deps--,f.deps===0&&f.resolve())},tn=(a,f,h,v=!1,g=!1,_=0)=>{for(let S=_;S{if(a.shapeFlag&6)return Ln(a.component.subTree);if(a.shapeFlag&128)return a.suspense.next();const f=m(a.anchor||a.el),h=f&&f[No];return h?m(h):f};let Ts=!1;const Li=(a,f,h)=>{a==null?f._vnode&&je(f._vnode,null,null,!0):T(f._vnode||null,a,f,null,null,null,h),f._vnode=a,Ts||(Ts=!0,ir(),or(),Ts=!1)},nn={p:T,um:je,m:bt,r:$i,mt:ge,mc:we,pc:Y,pbc:W,n:Ln,o:e};return{render:Li,hydrate:void 0,createApp:al(Li)}}function gs({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function dt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function _l(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function jr(e,t,n=!1){const s=e.children,r=t.children;if(I(s)&&I(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function Vr(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Vr(t)}function Ur(e){if(e)for(let t=0;tJe(yl);function wl(e,t){return bs(e,null,{flush:"sync"})}function An(e,t,n){return bs(e,t,n)}function bs(e,t,n=j){const{immediate:s,deep:r,flush:i,once:o}=n,l=ie({},n),c=t&&s||!t&&i!=="post";let d;if(Qt){if(i==="sync"){const b=xl();d=b.__watcherHandles||(b.__watcherHandles=[])}else if(!c){const b=()=>{};return b.stop=De,b.resume=De,b.pause=De,b}}const u=pe;l.call=(b,E,T)=>Oe(b,u,E,T);let p=!1;i==="post"?l.scheduler=b=>{Te(b,u&&u.suspense)}:i!=="sync"&&(p=!0,l.scheduler=(b,E)=>{E?b():rs(b)}),l.augmentJob=b=>{t&&(b.flags|=4),p&&(b.flags|=2,u&&(b.id=u.uid,b.i=u))};const m=Po(e,t,l);return Qt&&(d?d.push(m):c&&m()),m}function Sl(e,t,n){const s=this.proxy,r=X(e)?e.includes(".")?qr(s,e):()=>s[e]:e.bind(s,s);let i;D(t)?i=t:(i=t.handler,n=t);const o=Zt(this),l=bs(r,i.bind(s),n);return o(),l}function qr(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;r{let u,p=j,m;return wl(()=>{const b=e[r];ye(u,b)&&(u=b,d())}),{get(){return c(),n.get?n.get(u):u},set(b){const E=n.set?n.set(b):b;if(!ye(E,u)&&!(p!==j&&ye(b,p)))return;const T=s.vnode.props;T&&(t in T||r in T||i in T)&&(`onUpdate:${t}`in T||`onUpdate:${r}`in T||`onUpdate:${i}`in T)||(u=b,d()),s.emit(`update:${t}`,E),ye(b,E)&&ye(b,p)&&!ye(E,m)&&d(),p=b,m=E}}});return l[Symbol.iterator]=()=>{let c=0;return{next(){return c<2?{value:c++?o||j:l,done:!1}:{done:!0}}}},l}const Wr=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Ee(t)}Modifiers`]||e[`${et(t)}Modifiers`];function El(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||j;let r=n;const i=t.startsWith("update:"),o=i&&Wr(s,t.slice(7));o&&(o.trim&&(r=n.map(u=>X(u)?u.trim():u)),o.number&&(r=n.map(Un)));let l,c=s[l=jn(t)]||s[l=jn(Ee(t))];!c&&i&&(c=s[l=jn(et(t))]),c&&Oe(c,e,6,r);const d=s[l+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Oe(d,e,6,r)}}function Kr(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!D(e)){const c=d=>{const u=Kr(d,t,!0);u&&(l=!0,ie(o,u))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(K(e)&&s.set(e,null),null):(I(i)?i.forEach(c=>o[c]=null):ie(o,i),K(e)&&s.set(e,o),o)}function Pn(e,t){return!e||!rn(t)?!1:(t=t.slice(2).replace(/Once$/,""),U(e,t[0].toLowerCase()+t.slice(1))||U(e,et(t))||U(e,t))}function Aa(){}function zr(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:d,renderCache:u,props:p,data:m,setupState:b,ctx:E,inheritAttrs:T}=e,R=wn(e);let O,V;try{if(n.shapeFlag&4){const k=r||s,J=k;O=Be(d.call(J,k,u,p,b,m,E)),V=l}else{const k=t;O=Be(k.length>1?k(p,{attrs:l,slots:o,emit:c}):k(p,null)),V=t.props?l:Tl(l)}}catch(k){Kt.length=0,yn(k,e,1),O=de(me)}let z=O;if(V&&T!==!1){const k=Object.keys(V),{shapeFlag:J}=z;k.length&&J&7&&(i&&k.some(Nn)&&(V=Fl(V,i)),z=rt(z,V,!1,!0))}return n.dirs&&(z=rt(z,null,!1,!0),z.dirs=z.dirs?z.dirs.concat(n.dirs):n.dirs),n.transition&&Vt(z,n.transition),O=z,wn(R),O}const Tl=e=>{let t;for(const n in e)(n==="class"||n==="style"||rn(n))&&((t||(t={}))[n]=e[n]);return t},Fl=(e,t)=>{const n={};for(const s in e)(!Nn(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Al(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,d=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?Gr(s,o,d):!!o;if(c&8){const u=t.dynamicProps;for(let p=0;pe.__isSuspense;function Il(e,t){t&&t.pendingBranch?I(e)?t.effects.push(...e):t.effects.push(e):Lo(e)}const re=Symbol.for("v-fgt"),In=Symbol.for("v-txt"),me=Symbol.for("v-cmt"),_s=Symbol.for("v-stc"),Kt=[];let Fe=null;function ee(e=!1){Kt.push(Fe=e?null:[])}function Ml(){Kt.pop(),Fe=Kt[Kt.length-1]||null}let zt=1;function Yr(e,t=!1){zt+=e,e<0&&Fe&&t&&(Fe.hasOnce=!0)}function Zr(e){return e.dynamicChildren=zt>0?Fe||vt:null,Ml(),zt>0&&Fe&&Fe.push(e),e}function le(e,t,n,s,r,i){return Zr(F(e,t,n,s,r,i,!0))}function Gt(e,t,n,s,r){return Zr(de(e,t,n,s,r,!0))}function Jt(e){return e?e.__v_isVNode===!0:!1}function pt(e,t){return e.type===t.type&&e.key===t.key}const Qr=({key:e})=>e??null,Mn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?X(e)||fe(e)||D(e)?{i:oe,r:e,k:t,f:!!n}:e:null);function F(e,t=null,n=null,s=0,r=null,i=e===re?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Qr(t),ref:t&&Mn(t),scopeId:cr,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:oe};return l?(ys(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=X(n)?8:16),zt>0&&!o&&Fe&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&Fe.push(c),c}const de=kl;function kl(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===xr)&&(e=me),Jt(e)){const l=rt(e,t,!0);return n&&ys(l,n),zt>0&&!i&&Fe&&(l.shapeFlag&6?Fe[Fe.indexOf(e)]=l:Fe.push(l)),l.patchFlag=-2,l}if(ql(e)&&(e=e.__vccOpts),t){t=Rl(t);let{class:l,style:c}=t;l&&!X(l)&&(t.class=ct(l)),K(c)&&(ns(c)&&!I(c)&&(c=ie({},c)),t.style=xt(c))}const o=X(e)?1:Jr(e)?128:ar(e)?64:K(e)?4:D(e)?2:0;return F(e,t,n,s,r,o,i,!0)}function Rl(e){return e?ns(e)||Or(e)?ie({},e):e:null}function rt(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,d=t?Ol(r||{},t):r,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&Qr(d),ref:t&&t.ref?n&&i?I(i)?i.concat(Mn(t)):[i,Mn(t)]:Mn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==re?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&rt(e.ssContent),ssFallback:e.ssFallback&&rt(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&Vt(u,c.clone(u)),u}function Yt(e=" ",t=0){return de(In,null,e,t)}function vs(e="",t=!1){return t?(ee(),Gt(me,null,e)):de(me,null,e)}function Be(e){return e==null||typeof e=="boolean"?de(me):I(e)?de(re,null,e.slice()):Jt(e)?it(e):de(In,null,String(e))}function it(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:rt(e)}function ys(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(I(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),ys(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!Or(t)?t._ctx=oe:r===3&&oe&&(oe.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else D(t)?(t={default:t,_ctx:oe},n=32):(t=String(t),s&64?(n=16,t=[Yt(t)]):n=8);e.children=t,e.shapeFlag|=n}function Ol(...e){const t={};for(let n=0;npe||oe;let kn,xs;{const e=un(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};kn=t("__VUE_INSTANCE_SETTERS__",n=>pe=n),xs=t("__VUE_SSR_SETTERS__",n=>Qt=n)}const Zt=e=>{const t=pe;return kn(e),e.scope.on(),()=>{e.scope.off(),kn(t)}},ei=()=>{pe&&pe.scope.off(),kn(null)};function ti(e){return e.vnode.shapeFlag&4}let Qt=!1;function Nl(e,t=!1,n=!1){t&&xs(t);const{props:s,children:r}=e.vnode,i=ti(e);ul(e,s,i,t),hl(e,r,n||t);const o=i?Bl(e,t):void 0;return t&&xs(!1),o}function Bl(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,nl);const{setup:s}=n;if(s){$e();const r=e.setupContext=s.length>1?jl(e):null,i=Zt(e),o=Et(s,e,0,[e.props,r]),l=As(o);if(Le(),i(),(l||e.sp)&&!Pt(e)&&gr(e),l){if(o.then(ei,ei),t)return o.then(c=>{ni(e,c)}).catch(c=>{yn(c,e,0)});e.asyncDep=o}else ni(e,o)}else si(e)}function ni(e,t,n){D(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:K(t)&&(e.setupState=tr(t)),si(e)}function si(e,t,n){const s=e.type;e.render||(e.render=s.render||De);{const r=Zt(e);$e();try{sl(e)}finally{Le(),r()}}}const Hl={get(e,t){return he(e,"get",""),e[t]}};function jl(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Hl),slots:e.slots,emit:e.emit,expose:t}}function Rn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(tr(yo(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in qt)return qt[n](e)},has(t,n){return n in t||n in qt}})):e.proxy}const Vl=/(?:^|[-_])(\w)/g,Ul=e=>e.replace(Vl,t=>t.toUpperCase()).replace(/[-_]/g,"");function ri(e,t=!0){return D(e)?e.displayName||e.name:e.name||t&&e.__name}function ii(e,t,n=!1){let s=ri(t);if(!s&&t.__file){const r=t.__file.match(/([^/\\]+)\.\w+$/);r&&(s=r[1])}if(!s&&e&&e.parent){const r=i=>{for(const o in i)if(i[o]===t)return o};s=r(e.components||e.parent.type.components)||r(e.appContext.components)}return s?Ul(s):n?"App":"Anonymous"}function ql(e){return D(e)&&"__vccOpts"in e}const oi=(e,t)=>Fo(e,t,Qt);function Wl(e,t,n){const s=arguments.length;return s===2?K(t)&&!I(t)?Jt(t)?de(e,null,[t]):de(e,t):de(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&Jt(n)&&(n=[n]),de(e,t,n))}const Kl="3.5.17";/** -* @vue/runtime-dom v3.5.17 +`,...Do(r)),console.warn(...i)}Ne(),is=!1}function $o(){let e=qt[qt.length-1];if(!e)return[];const t=[];for(;e;){const n=t[0];n&&n.vnode===e?n.recurseCount++:t.push({vnode:e,recurseCount:0});const s=e.component&&e.component.parent;e=s&&s.vnode}return t}function Do(e){const t=[];return e.forEach((n,s)=>{t.push(...s===0?[]:[` +`],...Lo(n))}),t}function Lo({vnode:e,recurseCount:t}){const n=t>0?`... (${t} recursive calls)`:"",s=e.component?e.component.parent==null:!1,r=` at <${fi(e.component,e.type,s)}`,i=">"+n;return e.props?[r,...No(e.props),i]:[r+i]}function No(e){const t=[],n=Object.keys(e);return n.slice(0,3).forEach(s=>{t.push(...ur(s,e[s]))}),n.length>3&&t.push(" ..."),t}function ur(e,t,n){return X(t)?(t=JSON.stringify(t),n?t:[`${e}=${t}`]):typeof t=="number"||typeof t=="boolean"||t==null?n?t:[`${e}=${t}`]:ue(t)?(t=ur(e,j(t.value),!0),n?t:[`${e}=Ref<`,t,">"]):R(t)?[`${e}=fn${t.name?`<${t.name}>`:""}`]:(t=j(t),n?t:[`${e}=`,t])}function Ft(e,t,n,s){try{return s?e(...s):e()}catch(r){Sn(r,t,n)}}function $e(e,t,n,s){if(R(e)){const r=Ft(e,t,n,s);return r&&$s(r)&&r.catch(i=>{Sn(i,t,n)}),r}if(I(e)){const r=[];for(let i=0;i>>1,r=be[s],i=Ut(r);i=Ut(n)?be.push(e):be.splice(jo(t),0,e),e.flags|=1,dr()}}function dr(){wn||(wn=fr.then(mr))}function Ho(e){I(e)?Et.push(...e):st&&e.id===-1?st.splice(At+1,0,e):e.flags&1||(Et.push(e),e.flags|=1),dr()}function pr(e,t,n=Be+1){for(;nUt(n)-Ut(s));if(Et.length=0,st){st.push(...t);return}for(st=t,At=0;Ate.id==null?e.flags&2?-1:1/0:e.id;function mr(e){try{for(Be=0;Be{s._d&&Mn(-1);const i=Cn(t);let o;try{o=e(...r)}finally{Cn(i),s._d&&Mn(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function Pt(e,t){if(fe===null)return e;const n=$n(fe),s=e.dirs||(e.dirs=[]);for(let r=0;re.__isTeleport,Je=Symbol("_leaveCb"),Tn=Symbol("_enterCb");function Uo(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Er(()=>{e.isMounted=!0}),Ar(()=>{e.isUnmounting=!0}),e}const ke=[Function,Array],vr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:ke,onEnter:ke,onAfterEnter:ke,onEnterCancelled:ke,onBeforeLeave:ke,onLeave:ke,onAfterLeave:ke,onLeaveCancelled:ke,onBeforeAppear:ke,onAppear:ke,onAfterAppear:ke,onAppearCancelled:ke},_r=e=>{const t=e.subTree;return t.component?_r(t.component):t},Wo={name:"BaseTransition",props:vr,setup(e,{slots:t}){const n=Cs(),s=Uo();return()=>{const r=t.default&&wr(t.default(),!0);if(!r||!r.length)return;const i=yr(r),o=j(e),{mode:l}=o;if(s.isLeaving)return as(i);const c=Sr(i);if(!c)return as(i);let d=cs(c,o,s,n,p=>d=p);c.type!==pe&&Wt(c,d);let u=n.subTree&&Sr(n.subTree);if(u&&u.type!==pe&&!ht(u,c)&&_r(n).type!==pe){let p=cs(u,o,s,n);if(Wt(u,p),l==="out-in"&&c.type!==pe)return s.isLeaving=!0,p.afterLeave=()=>{s.isLeaving=!1,n.job.flags&8||n.update(),delete p.afterLeave,u=void 0},as(i);l==="in-out"&&c.type!==pe?p.delayLeave=(m,v,A)=>{const F=xr(s,u);F[String(u.key)]=u,m[Je]=()=>{v(),m[Je]=void 0,delete d.delayedLeave,u=void 0},d.delayedLeave=()=>{A(),delete d.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return i}}};function yr(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==pe){t=n;break}}return t}const Ko=Wo;function xr(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function cs(e,t,n,s,r){const{appear:i,mode:o,persisted:l=!1,onBeforeEnter:c,onEnter:d,onAfterEnter:u,onEnterCancelled:p,onBeforeLeave:m,onLeave:v,onAfterLeave:A,onLeaveCancelled:F,onBeforeAppear:B,onAppear:k,onAfterAppear:L,onAppearCancelled:D}=t,P=String(e.key),W=xr(n,e),le=(N,z)=>{N&&$e(N,s,9,z)},_e=(N,z)=>{const ne=z[1];le(N,z),I(N)?N.every(E=>E.length<=1)&&ne():N.length<=1&&ne()},we={mode:o,persisted:l,beforeEnter(N){let z=c;if(!n.isMounted)if(i)z=B||c;else return;N[Je]&&N[Je](!0);const ne=W[P];ne&&ht(e,ne)&&ne.el[Je]&&ne.el[Je](),le(z,[N])},enter(N){let z=d,ne=u,E=p;if(!n.isMounted)if(i)z=k||d,ne=L||u,E=D||p;else return;let Q=!1;const me=N[Tn]=et=>{Q||(Q=!0,et?le(E,[N]):le(ne,[N]),we.delayedLeave&&we.delayedLeave(),N[Tn]=void 0)};z?_e(z,[N,me]):me()},leave(N,z){const ne=String(e.key);if(N[Tn]&&N[Tn](!0),n.isUnmounting)return z();le(m,[N]);let E=!1;const Q=N[Je]=me=>{E||(E=!0,z(),me?le(F,[N]):le(A,[N]),N[Je]=void 0,W[ne]===e&&delete W[ne])};W[ne]=e,v?_e(v,[N,Q]):Q()},clone(N){const z=cs(N,t,n,s,r);return r&&r(z),z}};return we}function as(e){if(En(e))return e=it(e),e.children=null,e}function Sr(e){if(!En(e))return br(e.type)&&e.children?yr(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&R(n.default))return n.default()}}function Wt(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Wt(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function wr(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;iKt(A,t&&(I(t)?t[F]:t),n,s,r));return}if(kt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Kt(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?$n(s.component):s.el,o=r?null:i,{i:l,r:c}=e,d=t&&t.r,u=l.refs===q?l.refs={}:l.refs,p=l.setupState,m=j(p),v=p===q?Os:A=>U(m,A);if(d!=null&&d!==c){if(Tr(t),X(d))u[d]=null,v(d)&&(p[d]=null);else if(ue(d)){d.value=null;const A=t;A.k&&(u[A.k]=null)}}if(R(c))Ft(c,l,12,[o,u]);else{const A=X(c),F=ue(c);if(A||F){const B=()=>{if(e.f){const k=A?v(c)?p[c]:u[c]:c.value;if(r)I(k)&&Hn(k,i);else if(I(k))k.includes(i)||k.push(i);else if(A)u[c]=[i],v(c)&&(p[c]=u[c]);else{const L=[i];c.value=L,e.k&&(u[e.k]=L)}}else A?(u[c]=o,v(c)&&(p[c]=o)):F&&(c.value=o,e.k&&(u[e.k]=o))};if(o){const k=()=>{B(),Fn.delete(e)};k.id=-1,Fn.set(e,k),Ee(k,n)}else Tr(e),B()}}}function Tr(e){const t=Fn.get(e);t&&(t.flags|=8,Fn.delete(e))}dn().requestIdleCallback,dn().cancelIdleCallback;const kt=e=>!!e.type.__asyncLoader,En=e=>e.type.__isKeepAlive;function zo(e,t){Fr(e,"a",t)}function Go(e,t){Fr(e,"da",t)}function Fr(e,t,n=he){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(An(t,s,n),n){let r=n.parent;for(;r&&r.parent;)En(r.parent.vnode)&&Jo(s,t,n,r),r=r.parent}}function Jo(e,t,n,s){const r=An(t,e,s,!0);Pr(()=>{Hn(s[t],r)},n)}function An(e,t,n=he,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{Le();const l=Xt(n),c=$e(t,n,e,o);return l(),Ne(),c});return s?r.unshift(i):r.push(i),i}}const Ye=e=>(t,n=he)=>{(!en||e==="sp")&&An(e,(...s)=>t(...s),n)},Yo=Ye("bm"),Er=Ye("m"),Zo=Ye("bu"),Qo=Ye("u"),Ar=Ye("bum"),Pr=Ye("um"),Xo=Ye("sp"),el=Ye("rtg"),tl=Ye("rtc");function nl(e,t=he){An("ec",e,t)}const sl="components",Ir=Symbol.for("v-ndc");function rl(e){return X(e)?il(sl,e,!1)||e:e||Ir}function il(e,t,n=!0,s=!1){const r=fe||he;if(r){const i=r.type;{const l=ui(i,!1);if(l&&(l===t||l===Fe(t)||l===un(Fe(t))))return i}const o=kr(r[e]||i[e],t)||kr(r.appContext[e],t);return!o&&s?i:o}}function kr(e,t){return e&&(e[t]||e[Fe(t)]||e[un(Fe(t))])}function us(e,t,n,s){let r;const i=n,o=I(e);if(o||X(e)){const l=o&&Tt(e);let c=!1,d=!1;l&&(c=!Ie(e),d=nt(e),e=hn(e)),r=new Array(e.length);for(let u=0,p=e.length;ut(l,c,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,d=l.length;cQt(t)?!(t.type===pe||t.type===re&&!Mr(t.children)):!0)?e:null}const fs=e=>e?li(e)?$n(e):fs(e.parent):null,zt=ie(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>fs(e.parent),$root:e=>fs(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Dr(e),$forceUpdate:e=>e.f||(e.f=()=>{os(e.update)}),$nextTick:e=>e.n||(e.n=Vo.bind(e.proxy)),$watch:e=>Al.bind(e)}),ds=(e,t)=>e!==q&&!e.__isScriptSetup&&U(e,t),ll={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;let d;if(t[0]!=="$"){const v=o[t];if(v!==void 0)switch(v){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(ds(s,t))return o[t]=1,s[t];if(r!==q&&U(r,t))return o[t]=2,r[t];if((d=e.propsOptions[0])&&U(d,t))return o[t]=3,i[t];if(n!==q&&U(n,t))return o[t]=4,n[t];ps&&(o[t]=0)}}const u=zt[t];let p,m;if(u)return t==="$attrs"&&de(e.attrs,"get",""),u(e);if((p=l.__cssModules)&&(p=p[t]))return p;if(n!==q&&U(n,t))return o[t]=4,n[t];if(m=c.config.globalProperties,U(m,t))return m[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return ds(r,t)?(r[t]=n,!0):s!==q&&U(s,t)?(s[t]=n,!0):U(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i,type:o}},l){let c,d;return!!(n[l]||e!==q&&l[0]!=="$"&&U(e,l)||ds(t,l)||(c=i[0])&&U(c,l)||U(s,l)||U(zt,l)||U(r.config.globalProperties,l)||(d=o.__cssModules)&&d[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:U(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Or(e){return I(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let ps=!0;function cl(e){const t=Dr(e),n=e.proxy,s=e.ctx;ps=!1,t.beforeCreate&&Rr(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:d,created:u,beforeMount:p,mounted:m,beforeUpdate:v,updated:A,activated:F,deactivated:B,beforeDestroy:k,beforeUnmount:L,destroyed:D,unmounted:P,render:W,renderTracked:le,renderTriggered:_e,errorCaptured:we,serverPrefetch:N,expose:z,inheritAttrs:ne,components:E,directives:Q,filters:me}=t;if(d&&al(d,s,null),o)for(const se in o){const Y=o[se];R(Y)&&(s[se]=Y.bind(n))}if(r){const se=r.call(n,n);G(se)&&(e.data=ss(se))}if(ps=!0,i)for(const se in i){const Y=i[se],bt=R(Y)?Y.bind(n,n):R(Y.get)?Y.get.bind(n,n):De,Bn=!R(Y)&&R(Y.set)?Y.set.bind(n):De,vt=Fs({get:bt,set:Bn});Object.defineProperty(s,se,{enumerable:!0,configurable:!0,get:()=>vt.value,set:He=>vt.value=He})}if(l)for(const se in l)$r(l[se],s,n,se);if(c){const se=R(c)?c.call(n):c;Reflect.ownKeys(se).forEach(Y=>{ms(Y,se[Y])})}u&&Rr(u,e,"c");function ce(se,Y){I(Y)?Y.forEach(bt=>se(bt.bind(n))):Y&&se(Y.bind(n))}if(ce(Yo,p),ce(Er,m),ce(Zo,v),ce(Qo,A),ce(zo,F),ce(Go,B),ce(nl,we),ce(tl,le),ce(el,_e),ce(Ar,L),ce(Pr,P),ce(Xo,N),I(z))if(z.length){const se=e.exposed||(e.exposed={});z.forEach(Y=>{Object.defineProperty(se,Y,{get:()=>n[Y],set:bt=>n[Y]=bt,enumerable:!0})})}else e.exposed||(e.exposed={});W&&e.render===De&&(e.render=W),ne!=null&&(e.inheritAttrs=ne),E&&(e.components=E),Q&&(e.directives=Q),N&&Cr(e)}function al(e,t,n=De){I(e)&&(e=hs(e));for(const s in e){const r=e[s];let i;G(r)?"default"in r?i=Me(r.from||s,r.default,!0):i=Me(r.from||s):i=Me(r),ue(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function Rr(e,t,n){$e(I(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function $r(e,t,n,s){let r=s.includes(".")?Qr(n,s):()=>n[s];if(X(e)){const i=t[e];R(i)&&rt(r,i)}else if(R(e))rt(r,e.bind(n));else if(G(e))if(I(e))e.forEach(i=>$r(i,t,n,s));else{const i=R(e.handler)?e.handler.bind(n):t[e.handler];R(i)&&rt(r,i,e)}}function Dr(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(d=>Pn(c,d,o,!0)),Pn(c,t,o)),G(t)&&i.set(t,c),c}function Pn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Pn(e,i,n,!0),r&&r.forEach(o=>Pn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=ul[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const ul={data:Lr,props:Nr,emits:Nr,methods:Gt,computed:Gt,beforeCreate:ve,created:ve,beforeMount:ve,mounted:ve,beforeUpdate:ve,updated:ve,beforeDestroy:ve,beforeUnmount:ve,destroyed:ve,unmounted:ve,activated:ve,deactivated:ve,errorCaptured:ve,serverPrefetch:ve,components:Gt,directives:Gt,watch:dl,provide:Lr,inject:fl};function Lr(e,t){return t?e?function(){return ie(R(e)?e.call(this,this):e,R(t)?t.call(this,this):t)}:t:e}function fl(e,t){return Gt(hs(e),hs(t))}function hs(e){if(I(e)){const t={};for(let n=0;n1)return n&&R(t)?t.call(s&&s.proxy):t}}const Vr={},jr=()=>Object.create(Vr),Hr=e=>Object.getPrototypeOf(e)===Vr;function ml(e,t,n,s=!1){const r={},i=jr();e.propsDefaults=Object.create(null),qr(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:Co(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function gl(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=j(r),[c]=e.propsOptions;let d=!1;if((s||o>0)&&!(o&16)){if(o&8){const u=e.vnode.dynamicProps;for(let p=0;p{c=!0;const[m,v]=Ur(p,t,!0);ie(o,m),v&&l.push(...v)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!i&&!c)return G(e)&&s.set(e,yt),yt;if(I(i))for(let u=0;ue==="_"||e==="_ctx"||e==="$stable",vs=e=>I(e)?e.map(Ve):[Ve(e)],vl=(e,t,n)=>{if(t._n)return t;const s=ls((...r)=>vs(t(...r)),n);return s._c=!1,s},Kr=(e,t,n)=>{const s=e._ctx;for(const r in e){if(bs(r))continue;const i=e[r];if(R(i))t[r]=vl(r,i,s);else if(i!=null){const o=vs(i);t[r]=()=>o}}},zr=(e,t)=>{const n=vs(t);e.slots.default=()=>n},Gr=(e,t,n)=>{for(const s in t)(n||!bs(s))&&(e[s]=t[s])},_l=(e,t,n)=>{const s=e.slots=jr();if(e.vnode.shapeFlag&32){const r=t._;r?(Gr(s,t,n),n&&Ns(s,"_",r,!0)):Kr(t,s)}else t&&zr(e,t)},yl=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=q;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:Gr(r,t,n):(i=!t.$stable,Kr(t,r)),o=t}else t&&(zr(e,t),o={default:1});if(i)for(const l in r)!bs(l)&&o[l]==null&&delete r[l]},Ee=Dl;function xl(e){return Sl(e)}function Sl(e,t){const n=dn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:d,setElementText:u,parentNode:p,nextSibling:m,setScopeId:v=De,insertStaticContent:A}=e,F=(a,f,h,_=null,g=null,b=null,w=void 0,S=null,x=!!f.dynamicChildren)=>{if(a===f)return;a&&!ht(a,f)&&(_=Vn(a),He(a,g,b,!0),a=null),f.patchFlag===-2&&(x=!1,f.dynamicChildren=null);const{type:y,ref:O,shapeFlag:C}=f;switch(y){case kn:B(a,f,h,_);break;case pe:k(a,f,h,_);break;case xs:a==null&&L(f,h,_,w);break;case re:E(a,f,h,_,g,b,w,S,x);break;default:C&1?W(a,f,h,_,g,b,w,S,x):C&6?Q(a,f,h,_,g,b,w,S,x):(C&64||C&128)&&y.process(a,f,h,_,g,b,w,S,x,rn)}O!=null&&g?Kt(O,a&&a.ref,b,f||a,!f):O==null&&a&&a.ref!=null&&Kt(a.ref,null,b,a,!0)},B=(a,f,h,_)=>{if(a==null)s(f.el=l(f.children),h,_);else{const g=f.el=a.el;f.children!==a.children&&d(g,f.children)}},k=(a,f,h,_)=>{a==null?s(f.el=c(f.children||""),h,_):f.el=a.el},L=(a,f,h,_)=>{[a.el,a.anchor]=A(a.children,f,h,_,a.el,a.anchor)},D=({el:a,anchor:f},h,_)=>{let g;for(;a&&a!==f;)g=m(a),s(a,h,_),a=g;s(f,h,_)},P=({el:a,anchor:f})=>{let h;for(;a&&a!==f;)h=m(a),r(a),a=h;r(f)},W=(a,f,h,_,g,b,w,S,x)=>{f.type==="svg"?w="svg":f.type==="math"&&(w="mathml"),a==null?le(f,h,_,g,b,w,S,x):N(a,f,g,b,w,S,x)},le=(a,f,h,_,g,b,w,S)=>{let x,y;const{props:O,shapeFlag:C,transition:M,dirs:$}=a;if(x=a.el=o(a.type,b,O&&O.is,O),C&8?u(x,a.children):C&16&&we(a.children,x,null,_,g,_s(a,b),w,S),$&&dt(a,null,_,"created"),_e(x,a,a.scopeId,w,_),O){for(const Z in O)Z!=="value"&&!Lt(Z)&&i(x,Z,null,O[Z],b,_);"value"in O&&i(x,"value",null,O.value,b),(y=O.onVnodeBeforeMount)&&je(y,_,a)}$&&dt(a,null,_,"beforeMount");const V=wl(g,M);V&&M.beforeEnter(x),s(x,f,h),((y=O&&O.onVnodeMounted)||V||$)&&Ee(()=>{y&&je(y,_,a),V&&M.enter(x),$&&dt(a,null,_,"mounted")},g)},_e=(a,f,h,_,g)=>{if(h&&v(a,h),_)for(let b=0;b<_.length;b++)v(a,_[b]);if(g){let b=g.subTree;if(f===b||si(b.type)&&(b.ssContent===f||b.ssFallback===f)){const w=g.vnode;_e(a,w,w.scopeId,w.slotScopeIds,g.parent)}}},we=(a,f,h,_,g,b,w,S,x=0)=>{for(let y=x;y{const S=f.el=a.el;let{patchFlag:x,dynamicChildren:y,dirs:O}=f;x|=a.patchFlag&16;const C=a.props||q,M=f.props||q;let $;if(h&&pt(h,!1),($=M.onVnodeBeforeUpdate)&&je($,h,f,a),O&&dt(f,a,h,"beforeUpdate"),h&&pt(h,!0),(C.innerHTML&&M.innerHTML==null||C.textContent&&M.textContent==null)&&u(S,""),y?z(a.dynamicChildren,y,S,h,_,_s(f,g),b):w||Y(a,f,S,null,h,_,_s(f,g),b,!1),x>0){if(x&16)ne(S,C,M,h,g);else if(x&2&&C.class!==M.class&&i(S,"class",null,M.class,g),x&4&&i(S,"style",C.style,M.style,g),x&8){const V=f.dynamicProps;for(let Z=0;Z{$&&je($,h,f,a),O&&dt(f,a,h,"updated")},_)},z=(a,f,h,_,g,b,w)=>{for(let S=0;S{if(f!==h){if(f!==q)for(const b in f)!Lt(b)&&!(b in h)&&i(a,b,f[b],null,g,_);for(const b in h){if(Lt(b))continue;const w=h[b],S=f[b];w!==S&&b!=="value"&&i(a,b,S,w,g,_)}"value"in h&&i(a,"value",f.value,h.value,g)}},E=(a,f,h,_,g,b,w,S,x)=>{const y=f.el=a?a.el:l(""),O=f.anchor=a?a.anchor:l("");let{patchFlag:C,dynamicChildren:M,slotScopeIds:$}=f;$&&(S=S?S.concat($):$),a==null?(s(y,h,_),s(O,h,_),we(f.children||[],h,O,g,b,w,S,x)):C>0&&C&64&&M&&a.dynamicChildren?(z(a.dynamicChildren,M,h,g,b,w,S),(f.key!=null||g&&f===g.subTree)&&Jr(a,f,!0)):Y(a,f,h,O,g,b,w,S,x)},Q=(a,f,h,_,g,b,w,S,x)=>{f.slotScopeIds=S,a==null?f.shapeFlag&512?g.ctx.activate(f,h,_,w,x):me(f,h,_,g,b,w,x):et(a,f,x)},me=(a,f,h,_,g,b,w)=>{const S=a.component=ql(a,_,g);if(En(a)&&(S.ctx.renderer=rn),Ul(S,!1,w),S.asyncDep){if(g&&g.registerDep(S,ce,w),!a.el){const x=S.subTree=Se(pe);k(null,x,f,h),a.placeholder=x.el}}else ce(S,a,f,h,g,b,w)},et=(a,f,h)=>{const _=f.component=a.component;if(Rl(a,f,h))if(_.asyncDep&&!_.asyncResolved){se(_,f,h);return}else _.next=f,_.update();else f.el=a.el,_.vnode=f},ce=(a,f,h,_,g,b,w)=>{const S=()=>{if(a.isMounted){let{next:C,bu:M,u:$,parent:V,vnode:Z}=a;{const Ue=Yr(a);if(Ue){C&&(C.el=Z.el,se(a,C,w)),Ue.asyncDep.then(()=>{a.isUnmounted||S()});return}}let K=C,Ce;pt(a,!1),C?(C.el=Z.el,se(a,C,w)):C=Z,M&&fn(M),(Ce=C.props&&C.props.onVnodeBeforeUpdate)&&je(Ce,V,C,Z),pt(a,!0);const Te=ti(a),qe=a.subTree;a.subTree=Te,F(qe,Te,p(qe.el),Vn(qe),a,g,b),C.el=Te.el,K===null&&$l(a,Te.el),$&&Ee($,g),(Ce=C.props&&C.props.onVnodeUpdated)&&Ee(()=>je(Ce,V,C,Z),g)}else{let C;const{el:M,props:$}=f,{bm:V,m:Z,parent:K,root:Ce,type:Te}=a,qe=kt(f);pt(a,!1),V&&fn(V),!qe&&(C=$&&$.onVnodeBeforeMount)&&je(C,K,f),pt(a,!0);{Ce.ce&&Ce.ce._def.shadowRoot!==!1&&Ce.ce._injectChildStyle(Te);const Ue=a.subTree=ti(a);F(null,Ue,h,_,a,g,b),f.el=Ue.el}if(Z&&Ee(Z,g),!qe&&(C=$&&$.onVnodeMounted)){const Ue=f;Ee(()=>je(C,K,Ue),g)}(f.shapeFlag&256||K&&kt(K.vnode)&&K.vnode.shapeFlag&256)&&a.a&&Ee(a.a,g),a.isMounted=!0,f=h=_=null}};a.scope.on();const x=a.effect=new Us(S);a.scope.off();const y=a.update=x.run.bind(x),O=a.job=x.runIfDirty.bind(x);O.i=a,O.id=a.uid,x.scheduler=()=>os(O),pt(a,!0),y()},se=(a,f,h)=>{f.component=a;const _=a.vnode.props;a.vnode=f,a.next=null,gl(a,f.props,_,h),yl(a,f.children,h),Le(),pr(a),Ne()},Y=(a,f,h,_,g,b,w,S,x=!1)=>{const y=a&&a.children,O=a?a.shapeFlag:0,C=f.children,{patchFlag:M,shapeFlag:$}=f;if(M>0){if(M&128){Bn(y,C,h,_,g,b,w,S,x);return}else if(M&256){bt(y,C,h,_,g,b,w,S,x);return}}$&8?(O&16&&sn(y,g,b),C!==y&&u(h,C)):O&16?$&16?Bn(y,C,h,_,g,b,w,S,x):sn(y,g,b,!0):(O&8&&u(h,""),$&16&&we(C,h,_,g,b,w,S,x))},bt=(a,f,h,_,g,b,w,S,x)=>{a=a||yt,f=f||yt;const y=a.length,O=f.length,C=Math.min(y,O);let M;for(M=0;MO?sn(a,g,b,!0,!1,C):we(f,h,_,g,b,w,S,x,C)},Bn=(a,f,h,_,g,b,w,S,x)=>{let y=0;const O=f.length;let C=a.length-1,M=O-1;for(;y<=C&&y<=M;){const $=a[y],V=f[y]=x?ot(f[y]):Ve(f[y]);if(ht($,V))F($,V,h,null,g,b,w,S,x);else break;y++}for(;y<=C&&y<=M;){const $=a[C],V=f[M]=x?ot(f[M]):Ve(f[M]);if(ht($,V))F($,V,h,null,g,b,w,S,x);else break;C--,M--}if(y>C){if(y<=M){const $=M+1,V=$M)for(;y<=C;)He(a[y],g,b,!0),y++;else{const $=y,V=y,Z=new Map;for(y=V;y<=M;y++){const Pe=f[y]=x?ot(f[y]):Ve(f[y]);Pe.key!=null&&Z.set(Pe.key,y)}let K,Ce=0;const Te=M-V+1;let qe=!1,Ue=0;const on=new Array(Te);for(y=0;y=Te){He(Pe,g,b,!0);continue}let We;if(Pe.key!=null)We=Z.get(Pe.key);else for(K=V;K<=M;K++)if(on[K-V]===0&&ht(Pe,f[K])){We=K;break}We===void 0?He(Pe,g,b,!0):(on[We-V]=y+1,We>=Ue?Ue=We:qe=!0,F(Pe,f[We],h,null,g,b,w,S,x),Ce++)}const qi=qe?Cl(on):yt;for(K=qi.length-1,y=Te-1;y>=0;y--){const Pe=V+y,We=f[Pe],Ui=f[Pe+1],Wi=Pe+1{const{el:b,type:w,transition:S,children:x,shapeFlag:y}=a;if(y&6){vt(a.component.subTree,f,h,_);return}if(y&128){a.suspense.move(f,h,_);return}if(y&64){w.move(a,f,h,rn);return}if(w===re){s(b,f,h);for(let C=0;CS.enter(b),g);else{const{leave:C,delayLeave:M,afterLeave:$}=S,V=()=>{a.ctx.isUnmounted?r(b):s(b,f,h)},Z=()=>{b._isLeaving&&b[Je](!0),C(b,()=>{V(),$&&$()})};M?M(b,V,Z):Z()}else s(b,f,h)},He=(a,f,h,_=!1,g=!1)=>{const{type:b,props:w,ref:S,children:x,dynamicChildren:y,shapeFlag:O,patchFlag:C,dirs:M,cacheIndex:$}=a;if(C===-2&&(g=!1),S!=null&&(Le(),Kt(S,null,h,a,!0),Ne()),$!=null&&(f.renderCache[$]=void 0),O&256){f.ctx.deactivate(a);return}const V=O&1&&M,Z=!kt(a);let K;if(Z&&(K=w&&w.onVnodeBeforeUnmount)&&je(K,f,a),O&6)Ma(a.component,h,_);else{if(O&128){a.suspense.unmount(h,_);return}V&&dt(a,null,f,"beforeUnmount"),O&64?a.type.remove(a,f,h,rn,_):y&&!y.hasOnce&&(b!==re||C>0&&C&64)?sn(y,f,h,!1,!0):(b===re&&C&384||!g&&O&16)&&sn(x,f,h),_&&ji(a)}(Z&&(K=w&&w.onVnodeUnmounted)||V)&&Ee(()=>{K&&je(K,f,a),V&&dt(a,null,f,"unmounted")},h)},ji=a=>{const{type:f,el:h,anchor:_,transition:g}=a;if(f===re){ka(h,_);return}if(f===xs){P(a);return}const b=()=>{r(h),g&&!g.persisted&&g.afterLeave&&g.afterLeave()};if(a.shapeFlag&1&&g&&!g.persisted){const{leave:w,delayLeave:S}=g,x=()=>w(h,b);S?S(a.el,b,x):x()}else b()},ka=(a,f)=>{let h;for(;a!==f;)h=m(a),r(a),a=h;r(f)},Ma=(a,f,h)=>{const{bum:_,scope:g,job:b,subTree:w,um:S,m:x,a:y}=a;Zr(x),Zr(y),_&&fn(_),g.stop(),b&&(b.flags|=8,He(w,a,f,h)),S&&Ee(S,f),Ee(()=>{a.isUnmounted=!0},f)},sn=(a,f,h,_=!1,g=!1,b=0)=>{for(let w=b;w{if(a.shapeFlag&6)return Vn(a.component.subTree);if(a.shapeFlag&128)return a.suspense.next();const f=m(a.anchor||a.el),h=f&&f[qo];return h?m(h):f};let Ms=!1;const Hi=(a,f,h)=>{a==null?f._vnode&&He(f._vnode,null,null,!0):F(f._vnode||null,a,f,null,null,null,h),f._vnode=a,Ms||(Ms=!0,pr(),hr(),Ms=!1)},rn={p:F,um:He,m:vt,r:ji,mt:me,mc:we,pc:Y,pbc:z,n:Vn,o:e};return{render:Hi,hydrate:void 0,createApp:hl(Hi)}}function _s({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function pt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function wl(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Jr(e,t,n=!1){const s=e.children,r=t.children;if(I(s)&&I(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function Yr(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Yr(t)}function Zr(e){if(e)for(let t=0;tMe(Tl);function El(e,t){return ys(e,null,{flush:"sync"})}function rt(e,t,n){return ys(e,t,n)}function ys(e,t,n=q){const{immediate:s,deep:r,flush:i,once:o}=n,l=ie({},n),c=t&&s||!t&&i!=="post";let d;if(en){if(i==="sync"){const v=Fl();d=v.__watcherHandles||(v.__watcherHandles=[])}else if(!c){const v=()=>{};return v.stop=De,v.resume=De,v.pause=De,v}}const u=he;l.call=(v,A,F)=>$e(v,u,A,F);let p=!1;i==="post"?l.scheduler=v=>{Ee(v,u&&u.suspense)}:i!=="sync"&&(p=!0,l.scheduler=(v,A)=>{A?v():os(v)}),l.augmentJob=v=>{t&&(v.flags|=4),p&&(v.flags|=2,u&&(v.id=u.uid,v.i=u))};const m=Ro(e,t,l);return en&&(d?d.push(m):c&&m()),m}function Al(e,t,n){const s=this.proxy,r=X(e)?e.includes(".")?Qr(s,e):()=>s[e]:e.bind(s,s);let i;R(t)?i=t:(i=t.handler,n=t);const o=Xt(this),l=ys(r,i.bind(s),n);return o(),l}function Qr(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;r{let u,p=q,m;return El(()=>{const v=e[r];ye(u,v)&&(u=v,d())}),{get(){return c(),n.get?n.get(u):u},set(v){const A=n.set?n.set(v):v;if(!ye(A,u)&&!(p!==q&&ye(v,p)))return;const F=s.vnode.props;F&&(t in F||r in F||i in F)&&(`onUpdate:${t}`in F||`onUpdate:${r}`in F||`onUpdate:${i}`in F)||(u=v,d()),s.emit(`update:${t}`,A),ye(v,A)&&ye(v,p)&&!ye(A,m)&&d(),p=v,m=A}}});return l[Symbol.iterator]=()=>{let c=0;return{next(){return c<2?{value:c++?o||q:l,done:!1}:{done:!0}}}},l}const Xr=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Fe(t)}Modifiers`]||e[`${tt(t)}Modifiers`];function Il(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||q;let r=n;const i=t.startsWith("update:"),o=i&&Xr(s,t.slice(7));o&&(o.trim&&(r=n.map(u=>X(u)?u.trim():u)),o.number&&(r=n.map(Wn)));let l,c=s[l=Un(t)]||s[l=Un(Fe(t))];!c&&i&&(c=s[l=Un(tt(t))]),c&&$e(c,e,6,r);const d=s[l+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,$e(d,e,6,r)}}const kl=new WeakMap;function ei(e,t,n=!1){const s=n?kl:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!R(e)){const c=d=>{const u=ei(d,t,!0);u&&(l=!0,ie(o,u))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(G(e)&&s.set(e,null),null):(I(i)?i.forEach(c=>o[c]=null):ie(o,i),G(e)&&s.set(e,o),o)}function In(e,t){return!e||!ln(t)?!1:(t=t.slice(2).replace(/Once$/,""),U(e,t[0].toLowerCase()+t.slice(1))||U(e,tt(t))||U(e,t))}function Da(){}function ti(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:d,renderCache:u,props:p,data:m,setupState:v,ctx:A,inheritAttrs:F}=e,B=Cn(e);let k,L;try{if(n.shapeFlag&4){const P=r||s,W=P;k=Ve(d.call(W,P,u,p,v,m,A)),L=l}else{const P=t;k=Ve(P.length>1?P(p,{attrs:l,slots:o,emit:c}):P(p,null)),L=t.props?l:Ml(l)}}catch(P){Jt.length=0,Sn(P,e,1),k=Se(pe)}let D=k;if(L&&F!==!1){const P=Object.keys(L),{shapeFlag:W}=D;P.length&&W&7&&(i&&P.some(jn)&&(L=Ol(L,i)),D=it(D,L,!1,!0))}return n.dirs&&(D=it(D,null,!1,!0),D.dirs=D.dirs?D.dirs.concat(n.dirs):n.dirs),n.transition&&Wt(D,n.transition),k=D,Cn(B),k}const Ml=e=>{let t;for(const n in e)(n==="class"||n==="style"||ln(n))&&((t||(t={}))[n]=e[n]);return t},Ol=(e,t)=>{const n={};for(const s in e)(!jn(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Rl(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,d=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?ni(s,o,d):!!o;if(c&8){const u=t.dynamicProps;for(let p=0;pe.__isSuspense;function Dl(e,t){t&&t.pendingBranch?I(e)?t.effects.push(...e):t.effects.push(e):Ho(e)}const re=Symbol.for("v-fgt"),kn=Symbol.for("v-txt"),pe=Symbol.for("v-cmt"),xs=Symbol.for("v-stc"),Jt=[];let Ae=null;function te(e=!1){Jt.push(Ae=e?null:[])}function Ll(){Jt.pop(),Ae=Jt[Jt.length-1]||null}let Yt=1;function Mn(e,t=!1){Yt+=e,e<0&&Ae&&t&&(Ae.hasOnce=!0)}function ri(e){return e.dynamicChildren=Yt>0?Ae||yt:null,Ll(),Yt>0&&Ae&&Ae.push(e),e}function oe(e,t,n,s,r,i){return ri(T(e,t,n,s,r,i,!0))}function Zt(e,t,n,s,r){return ri(Se(e,t,n,s,r,!0))}function Qt(e){return e?e.__v_isVNode===!0:!1}function ht(e,t){return e.type===t.type&&e.key===t.key}const ii=({key:e})=>e??null,On=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?X(e)||ue(e)||R(e)?{i:fe,r:e,k:t,f:!!n}:e:null);function T(e,t=null,n=null,s=0,r=null,i=e===re?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ii(t),ref:t&&On(t),scopeId:gr,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:fe};return l?(ws(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=X(n)?8:16),Yt>0&&!o&&Ae&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&Ae.push(c),c}const Se=Nl;function Nl(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Ir)&&(e=pe),Qt(e)){const l=it(e,t,!0);return n&&ws(l,n),Yt>0&&!i&&Ae&&(l.shapeFlag&6?Ae[Ae.indexOf(e)]=l:Ae.push(l)),l.patchFlag=-2,l}if(Yl(e)&&(e=e.__vccOpts),t){t=Bl(t);let{class:l,style:c}=t;l&&!X(l)&&(t.class=at(l)),G(c)&&(rs(c)&&!I(c)&&(c=ie({},c)),t.style=St(c))}const o=X(e)?1:si(e)?128:br(e)?64:G(e)?4:R(e)?2:0;return T(e,t,n,s,r,o,i,!0)}function Bl(e){return e?rs(e)||Hr(e)?ie({},e):e:null}function it(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,d=t?Vl(r||{},t):r,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&ii(d),ref:t&&t.ref?n&&i?I(i)?i.concat(On(t)):[i,On(t)]:On(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==re?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&it(e.ssContent),ssFallback:e.ssFallback&&it(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&Wt(u,c.clone(u)),u}function Ot(e=" ",t=0){return Se(kn,null,e,t)}function Ss(e="",t=!1){return t?(te(),Zt(pe,null,e)):Se(pe,null,e)}function Ve(e){return e==null||typeof e=="boolean"?Se(pe):I(e)?Se(re,null,e.slice()):Qt(e)?ot(e):Se(kn,null,String(e))}function ot(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:it(e)}function ws(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(I(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),ws(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!Hr(t)?t._ctx=fe:r===3&&fe&&(fe.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else R(t)?(t={default:t,_ctx:fe},n=32):(t=String(t),s&64?(n=16,t=[Ot(t)]):n=8);e.children=t,e.shapeFlag|=n}function Vl(...e){const t={};for(let n=0;nhe||fe;let Rn,Ts;{const e=dn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};Rn=t("__VUE_INSTANCE_SETTERS__",n=>he=n),Ts=t("__VUE_SSR_SETTERS__",n=>en=n)}const Xt=e=>{const t=he;return Rn(e),e.scope.on(),()=>{e.scope.off(),Rn(t)}},oi=()=>{he&&he.scope.off(),Rn(null)};function li(e){return e.vnode.shapeFlag&4}let en=!1;function Ul(e,t=!1,n=!1){t&&Ts(t);const{props:s,children:r}=e.vnode,i=li(e);ml(e,s,i,t),_l(e,r,n||t);const o=i?Wl(e,t):void 0;return t&&Ts(!1),o}function Wl(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,ll);const{setup:s}=n;if(s){Le();const r=e.setupContext=s.length>1?zl(e):null,i=Xt(e),o=Ft(s,e,0,[e.props,r]),l=$s(o);if(Ne(),i(),(l||e.sp)&&!kt(e)&&Cr(e),l){if(o.then(oi,oi),t)return o.then(c=>{ci(e,c)}).catch(c=>{Sn(c,e,0)});e.asyncDep=o}else ci(e,o)}else ai(e)}function ci(e,t,n){R(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:G(t)&&(e.setupState=ar(t)),ai(e)}function ai(e,t,n){const s=e.type;e.render||(e.render=s.render||De);{const r=Xt(e);Le();try{cl(e)}finally{Ne(),r()}}}const Kl={get(e,t){return de(e,"get",""),e[t]}};function zl(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Kl),slots:e.slots,emit:e.emit,expose:t}}function $n(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(ar(To(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in zt)return zt[n](e)},has(t,n){return n in t||n in zt}})):e.proxy}const Gl=/(?:^|[-_])\w/g,Jl=e=>e.replace(Gl,t=>t.toUpperCase()).replace(/[-_]/g,"");function ui(e,t=!0){return R(e)?e.displayName||e.name:e.name||t&&e.__name}function fi(e,t,n=!1){let s=ui(t);if(!s&&t.__file){const r=t.__file.match(/([^/\\]+)\.\w+$/);r&&(s=r[1])}if(!s&&e&&e.parent){const r=i=>{for(const o in i)if(i[o]===t)return o};s=r(e.components||e.parent.type.components)||r(e.appContext.components)}return s?Jl(s):n?"App":"Anonymous"}function Yl(e){return R(e)&&"__vccOpts"in e}const Fs=(e,t)=>Mo(e,t,en);function Zl(e,t,n){const s=(i,o,l)=>{Mn(-1);try{return Se(i,o,l)}finally{Mn(1)}},r=arguments.length;return r===2?G(t)&&!I(t)?Qt(t)?s(e,null,[t]):s(e,t):s(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&Qt(n)&&(n=[n]),s(e,t,n))}const Ql="3.5.21";/** +* @vue/runtime-dom v3.5.21 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/let ws;const li=typeof window<"u"&&window.trustedTypes;if(li)try{ws=li.createPolicy("vue",{createHTML:e=>e})}catch{}const ci=ws?e=>ws.createHTML(e):e=>e,zl="http://www.w3.org/2000/svg",Gl="http://www.w3.org/1998/Math/MathML",Ye=typeof document<"u"?document:null,ai=Ye&&Ye.createElement("template"),Jl={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Ye.createElementNS(zl,e):t==="mathml"?Ye.createElementNS(Gl,e):n?Ye.createElement(e,{is:n}):Ye.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Ye.createTextNode(e),createComment:e=>Ye.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ye.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{ai.innerHTML=ci(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=ai.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},ot="transition",Xt="animation",en=Symbol("_vtc"),ui={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Yl=ie({},ur,ui),Zl=(e=>(e.displayName="Transition",e.props=Yl,e))((e,{slots:t})=>Wl(jo,Ql(e),t)),ht=(e,t=[])=>{I(e)?e.forEach(n=>n(...t)):e&&e(...t)},fi=e=>e?I(e)?e.some(t=>t.length>1):e.length>1:!1;function Ql(e){const t={};for(const A in e)A in ui||(t[A]=e[A]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:d=o,appearToClass:u=l,leaveFromClass:p=`${n}-leave-from`,leaveActiveClass:m=`${n}-leave-active`,leaveToClass:b=`${n}-leave-to`}=e,E=Xl(r),T=E&&E[0],R=E&&E[1],{onBeforeEnter:O,onEnter:V,onEnterCancelled:z,onLeave:k,onLeaveCancelled:J,onBeforeAppear:ce=O,onAppear:ve=V,onAppearCancelled:we=z}=t,L=(A,Q,ge,Xe)=>{A._enterCancelled=Xe,mt(A,Q?u:l),mt(A,Q?d:o),ge&&ge()},W=(A,Q)=>{A._isLeaving=!1,mt(A,p),mt(A,b),mt(A,m),Q&&Q()},te=A=>(Q,ge)=>{const Xe=A?ve:V,ae=()=>L(Q,A,ge);ht(Xe,[Q,ae]),di(()=>{mt(Q,A?c:i),Ze(Q,A?u:l),fi(Xe)||pi(Q,s,T,ae)})};return ie(t,{onBeforeEnter(A){ht(O,[A]),Ze(A,i),Ze(A,o)},onBeforeAppear(A){ht(ce,[A]),Ze(A,c),Ze(A,d)},onEnter:te(!1),onAppear:te(!0),onLeave(A,Q){A._isLeaving=!0;const ge=()=>W(A,Q);Ze(A,p),A._enterCancelled?(Ze(A,m),gi()):(gi(),Ze(A,m)),di(()=>{A._isLeaving&&(mt(A,p),Ze(A,b),fi(k)||pi(A,s,R,ge))}),ht(k,[A,ge])},onEnterCancelled(A){L(A,!1,void 0,!0),ht(z,[A])},onAppearCancelled(A){L(A,!0,void 0,!0),ht(we,[A])},onLeaveCancelled(A){W(A),ht(J,[A])}})}function Xl(e){if(e==null)return null;if(K(e))return[Ss(e.enter),Ss(e.leave)];{const t=Ss(e);return[t,t]}}function Ss(e){return Wi(e)}function Ze(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[en]||(e[en]=new Set)).add(t)}function mt(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[en];n&&(n.delete(t),n.size||(e[en]=void 0))}function di(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let ec=0;function pi(e,t,n,s){const r=e._endId=++ec,i=()=>{r===e._endId&&s()};if(n!=null)return setTimeout(i,n);const{type:o,timeout:l,propCount:c}=tc(e,t);if(!o)return s();const d=o+"end";let u=0;const p=()=>{e.removeEventListener(d,m),i()},m=b=>{b.target===e&&++u>=c&&p()};setTimeout(()=>{u(n[E]||"").split(", "),r=s(`${ot}Delay`),i=s(`${ot}Duration`),o=hi(r,i),l=s(`${Xt}Delay`),c=s(`${Xt}Duration`),d=hi(l,c);let u=null,p=0,m=0;t===ot?o>0&&(u=ot,p=o,m=i.length):t===Xt?d>0&&(u=Xt,p=d,m=c.length):(p=Math.max(o,d),u=p>0?o>d?ot:Xt:null,m=u?u===ot?i.length:c.length:0);const b=u===ot&&/\b(transform|all)(,|$)/.test(s(`${ot}Property`).toString());return{type:u,timeout:p,propCount:m,hasTransform:b}}function hi(e,t){for(;e.lengthmi(n)+mi(e[s])))}function mi(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function gi(){return document.body.offsetHeight}function nc(e,t,n){const s=e[en];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const bi=Symbol("_vod"),sc=Symbol("_vsh"),rc=Symbol(""),ic=/(^|;)\s*display\s*:/;function oc(e,t,n){const s=e.style,r=X(n);let i=!1;if(n&&!r){if(t)if(X(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&On(s,l,"")}else for(const o in t)n[o]==null&&On(s,o,"");for(const o in n)o==="display"&&(i=!0),On(s,o,n[o])}else if(r){if(t!==n){const o=s[rc];o&&(n+=";"+o),s.cssText=n,i=ic.test(n)}}else t&&e.removeAttribute("style");bi in e&&(e[bi]=i?s.display:"",e[sc]&&(s.display="none"))}const _i=/\s*!important$/;function On(e,t,n){if(I(n))n.forEach(s=>On(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=lc(e,t);_i.test(n)?e.setProperty(et(s),n.replace(_i,""),"important"):e[s]=n}}const vi=["Webkit","Moz","ms"],Cs={};function lc(e,t){const n=Cs[t];if(n)return n;let s=Ee(t);if(s!=="filter"&&s in e)return Cs[t]=s;s=cn(s);for(let r=0;rEs||(fc.then(()=>Es=0),Es=Date.now());function pc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;Oe(hc(s,n.value),t,5,[s])};return n.value=e,n.attached=dc(),n}function hc(e,t){if(I(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Ei=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,mc=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?nc(e,s,o):t==="style"?oc(e,n,s):rn(t)?Nn(t)||ac(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):gc(e,t,s,o))?(wi(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&xi(e,t,s,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!X(s))?wi(e,Ee(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),xi(e,t,s,o))};function gc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Ei(t)&&D(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Ei(t)&&X(n)?!1:t in e}const Mt=e=>{const t=e.props["onUpdate:modelValue"]||!1;return I(t)?n=>an(t,n):t};function bc(e){e.target.composing=!0}function Ti(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Qe=Symbol("_assign"),_c={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[Qe]=Mt(r);const i=s||r.props&&r.props.type==="number";lt(e,t?"change":"input",o=>{if(o.target.composing)return;let l=e.value;n&&(l=l.trim()),i&&(l=Un(l)),e[Qe](l)}),n&<(e,"change",()=>{e.value=e.value.trim()}),t||(lt(e,"compositionstart",bc),lt(e,"compositionend",Ti),lt(e,"change",Ti))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:i}},o){if(e[Qe]=Mt(o),e.composing)return;const l=(i||e.type==="number")&&!/^0\d/.test(e.value)?Un(e.value):e.value,c=t??"";l!==c&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c))}},Fi={deep:!0,created(e,t,n){e[Qe]=Mt(n),lt(e,"change",()=>{const s=e._modelValue,r=Pi(e),i=e.checked,o=e[Qe];if(I(s)){const l=Rs(s,r),c=l!==-1;if(i&&!c)o(s.concat(r));else if(!i&&c){const d=[...s];d.splice(l,1),o(d)}}else if(on(s)){const l=new Set(s);i?l.add(r):l.delete(r),o(l)}else o(Ii(e,i))})},mounted:Ai,beforeUpdate(e,t,n){e[Qe]=Mt(n),Ai(e,t,n)}};function Ai(e,{value:t,oldValue:n},s){e._modelValue=t;let r;if(I(t))r=Rs(t,s.props.value)>-1;else if(on(t))r=t.has(s.props.value);else{if(t===n)return;r=wt(t,Ii(e,!0))}e.checked!==r&&(e.checked=r)}const vc={created(e,{value:t},n){e.checked=wt(t,n.props.value),e[Qe]=Mt(n),lt(e,"change",()=>{e[Qe](Pi(e))})},beforeUpdate(e,{value:t,oldValue:n},s){e[Qe]=Mt(s),t!==n&&(e.checked=wt(t,s.props.value))}};function Pi(e){return"_value"in e?e._value:e.value}function Ii(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const yc=["ctrl","shift","alt","meta"],xc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>yc.some(n=>e[`${n}Key`]&&!t.includes(n))},Mi=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...i)=>{for(let o=0;o{const t=Sc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Tc(s);if(!r)return;const i=t._component;!D(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=n(r,!1,Ec(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t};function Ec(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Tc(e){return X(e)?document.querySelector(e):e}const Ri=Symbol("i18n");function kt(){return Je(Ri)}const Dn=Symbol("files"),Oi=Symbol("communication");class Fc{constructor(){this.listeners={}}emit(t,n){var s;(s=this.listeners[t])==null||s.forEach(r=>r(n))}on(t,n){this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(n)}off(t,n){if(!n){delete this.listeners[t];return}const s=this.listeners[t];if(s){const r=s.indexOf(n);r>-1&&s.splice(r,1)}}}function Ac(){return new Fc}const Pc={"mb-8":"",text:"12 neutral-800","nq-label":""},Ic={grid:"~ gap-16 cols-[repeat(auto-fit,128px)]","w-full":""},Mc=["src","alt"],kc=["aria-label","onClickCapture"],Rc={key:0,flex:"~ col justify-center items-center","rounded-4":"","size-128":"","aspect-square":"",outline:"1.5 neutral/15"},Oc={"font-semibold":"","mt-6":""},Dc={text:"f-xs neutral-800 center","mt-2":"","px-2":""},Di=At({__name:"AttachmentUploader",props:{maxFiles:{default:5}},setup(e){const{files:t,updateFiles:n}=Je(Dn),s=Re([]),r=Re([]),{t:i}=kt(),o=Re();An(t,d=>{d.length===0&&(s.value.forEach(u=>URL.revokeObjectURL(u)),s.value=[],r.value=[])},{deep:!0});function l(){if(!o.value)return;const d=[...t.value,...Array.from(o.value.files)].slice(0,e.maxFiles);n(d),s.value.forEach(u=>URL.revokeObjectURL(u)),s.value=[],r.value=[],t.value.forEach((u,p)=>{const m=URL.createObjectURL(u);s.value.push(m);const b=new Image;b.onload=()=>{const E=b.width/b.height;r.value[p]=E},b.src=m}),o.value.value=void 0}function c(d){URL.revokeObjectURL(s.value[d]),t.value.splice(d,1),s.value.splice(d,1),r.value.splice(d,1)}return(d,u)=>(ee(),le("label",{for:"attachments",class:ct({"cursor-pointer":H(t).length===0}),group:"","w-full":""},[F("h3",Pc,se(H(i)("attachmentUploader.title")),1),F("div",Ic,[(ee(!0),le(re,null,cs(s.value,(p,m)=>(ee(),le("div",{key:p,class:ct(["stack outline-1.5 outline-neutral-200 rounded-4 size-128!",[r.value[m]<1?"":"aspect-square"]]),style:xt(r.value[m]<1?{aspectRatio:r.value[m]}:{})},[F("img",{src:p,alt:H(i)("attachmentUploader.previewAlt",{number:m+1}),"h-full":"!","rounded-4":"","bg-neutral-100":"","max-h-128":"","object-contain":""},null,8,Mc),F("button",{type:"button","aria-label":H(i)("attachmentUploader.deleteImageLabel"),outline:"1.5 offset--1.5 white/8",stack:"","rounded-full":"","bg-white":"","size-24":"",shadow:"","self-start":"","right--12":"","top--12":"","justify-self-end":"",onClickCapture:Mi(b=>c(m),["stop"])},u[0]||(u[0]=[F("div",{"i-nimiq:cross":"",text:"neutral 10"},null,-1)]),40,kc)],6))),128)),H(t).length(ee(),le(re,null,[F("label",$c,[Sn(F("textarea",{id:"description","onUpdate:modelValue":i[0]||(i[0]=o=>s.value=o),name:"description",placeholder:H(t)("bugForm.descriptionPlaceholder"),rows:"4",required:"","nq-input-box":"","focus-visible:outline-blue":""},null,8,Lc),[[_c,s.value]])]),de(Di),F("label",Nc,[F("h3",Bc,se(H(t)("bugForm.emailLabel")),1),F("input",{id:"email","w-auto":"",type:"email","nq-input-box":"",name:"email",placeholder:H(t)("bugForm.emailPlaceholder")},null,8,Hc)]),F("label",jc,[Sn(F("input",{"onUpdate:modelValue":i[1]||(i[1]=o=>n.value=o),type:"checkbox",name:"shareDebugInfo","shrink-0":"","nq-switch":"","border-transparent":"!"},null,512),[[Fi,n.value]]),F("span",Vc,se(H(t)("bugForm.shareDebugInfoLabel")),1)])],64))}}),qc={flex:"~ items-center justify-center gap-24",outline:"[&:has(:focus-visible)]:1.5 blue","mx-auto":"","rounded-4":"","w-max":"","f-p-sm":""},Wc=["id","value"],Kc=["for","data-state"],zc={flex:"","f-mt-sm":""},Gc=["placeholder"],Jc=At({__name:"FeedbackForm",props:{modelValue:{default:0},modelModifiers:{}},emits:["update:modelValue"],setup(e){const t=Cl(e,"modelValue"),{t:n}=kt();return(s,r)=>(ee(),le(re,null,[F("div",qc,[(ee(),le(re,null,cs(5,i=>(ee(),le(re,{key:i},[Sn(F("input",{id:`rating-${i}`,"onUpdate:modelValue":r[0]||(r[0]=o=>t.value=o),class:"peer",type:"radio",name:"rating",value:i,"sr-only":""},null,8,Wc),[[vc,t.value]]),F("label",{for:`rating-${i}`,"data-state":i<=t.value?"active":void 0,text:"neutral-300 data-[state=active]:gold hocus:gold [&:has(~_label:hover)]:gold",style:xt(`--i: ${i}; --b:40ms`),delay:"[calc(25ms*var(--i))] group-hocus:[calc(var(--b)*(5-var(--i)))]","size-40":"","cursor-pointer":"","transition-colors":"","ease-out":"","i-nimiq:star":""},null,12,Kc)],64))),64))]),F("label",zc,[F("textarea",{id:"description",name:"description",placeholder:H(n)("feedbackForm.descriptionPlaceholder"),rows:"4",required:"","nq-input-box":""},null,8,Gc)])],64))}}),Yc={flex:"~ col","h-full":""},Zc={flex:"~ items-center gap-8","text-14":"","mb-16":"","h-max":"","w-full":"","text-balance":"","nq-label":""},Qc={key:0,role:"alert"},Xc=["data-app"],ea=["value"],ta=["value"],na=["value"],sa={flex:"~ items-center gap-8","f-text-sm":"","f-mt-sm":""},ra={"text-neutral-800":"","select-none":""},ia={"text-neutral-700":"","f-text-sm":"","f-mt-md":""},oa={href:"https://nimiq.com/privacy-policy/",target:"_blank","un-text-current":"",underline:""},la={key:0,role:"alert",text:"f-xs red-1100","font-semibold":""},ca={outline:"1.5 red-500","font-mono":"","font-normal":"","rounded-6":"","bg-red-400":"","f-p-2xs":""},aa={"mt-auto":"",flex:""},ua=["disabled"],fa={key:0,"i-nimiq:spinner":""},da=At({__name:"FormContainer",props:{type:{},app:{},feedbackEndpoint:{},tags:{default:()=>[]}},emits:["formSuccess","formError"],setup(e,{emit:t}){const n=t,{files:s}=Je(Dn),r=Je(Oi),i=Re(!1),{t:o}=kt(),l=Re(),c=Re("idle"),d=Re(),u={bug:"formContainer.titleBug",idea:"formContainer.titleIdea",feedback:"formContainer.titleFeedback"},p={bug:"i-nimiq:exclamation",idea:"i-nimiq:leaf-2-filled",feedback:"i-nimiq:star"},m={bug:"bg-gradient-red",idea:"bg-gradient-green",feedback:"bg-gradient-gold"};async function b(E){console.log("[Nimiq Feedback Widget] 📤 Starting form submission..."),c.value="pending";const T=E.target,R=new FormData(T);(e.type==="bug"||e.type==="idea")&&(console.log("Submitting feedback:",{type:R.get("type"),app:R.get("app"),attachments:s.value}),R.delete("attachments"),console.log("Files to be attached:",s.value),Array.from(s.value).forEach(V=>R.append("attachments",V)),console.log("Form data after appending files:",Array.from(R.entries()))),r==null||r.emit("before-submit",{formData:R,type:e.type,app:e.app});const O=await fetch(e.feedbackEndpoint,{method:"POST",body:R}).catch(V=>(c.value="error",V));if(!O.ok){console.log(`[Nimiq Feedback Widget] ❌ Form submission failed: ${O.status} ${O.statusText}`),c.value="error",l.value=await O.json(),n("formError",{error:l.value.message,details:l.value});return}d.value=await O.json(),c.value="success",console.log("[Nimiq Feedback Widget] ✅ Form submitted successfully"),n("formSuccess",d.value)}return(E,T)=>{var R;return ee(),le("div",Yc,[F("h2",Zc,[F("div",{class:ct(m[E.type]),stack:"","rounded-3":"","shrink-0":"","size-24":"",style:{"box-shadow":"0px 4px 16px 0px rgba(0, 0, 0, 0.07), 0px 1.5px 3px 0px rgba(0, 0, 0, 0.05), 0px 0.337px 2px 0px rgba(0, 0, 0, 0.03)"}},[F("div",{class:ct(p[E.type]),"text-white":""},null,2)],2),Yt(" "+se(H(o)(u[E.type])),1)]),c.value==="success"?(ee(),le("div",Qc,[F("p",null,se(H(o)("formContainer.successMessage")),1)])):(ee(),le("form",{key:1,flex:"~ col gap-16","px-1.5":"","h-full":"","data-app":E.app,onSubmit:Mi(b,["prevent"])},[F("input",{type:"text",name:"type",value:E.type,"sr-only":""},null,8,ea),F("input",{type:"text",name:"app",value:E.app,"sr-only":""},null,8,ta),F("input",{type:"text",name:"tags",value:E.tags.join(","),"sr-only":""},null,8,na),tl(E.$slots,"default"),F("label",sa,[Sn(F("input",{"onUpdate:modelValue":T[0]||(T[0]=O=>i.value=O),type:"checkbox",name:"acceptTerms","border-transparent":"!",required:"","shrink-0":"","nq-switch":""},null,512),[[Fi,i.value]]),F("span",ra,se(H(o)("feedbackWidget.termsAndConditionsApply")),1)]),F("p",ia,[F("a",oa,se(H(o)("formContainer.learnMore")),1),Yt(" "+se(H(o)("formContainer.privacyPolicyText")),1)]),c.value==="error"?(ee(),le("div",la,[F("p",null,[F("strong",null,se(H(o)("formContainer.errorPrefix")),1),Yt(" "+se((R=l.value)==null?void 0:R.message),1)]),(ee(!0),le(re,null,cs(l.value.issues,O=>(ee(),le("ul",{key:O,"list-disc":"","f-px-xs":""},[F("li",null,se(O),1)]))),128)),F("details",null,[F("summary",null,se(H(o)("formContainer.errorDetailsSummary")),1),F("pre",ca,se(l.value),1)])])):vs("",!0),F("div",aa,[F("button",{type:"submit",disabled:!i.value||c.value==="pending","mx-0":"","mb-0":"","w-full":"","nq-pill-xl":"","nq-pill-blue":"","disabled:op-60":"",style:{"background-image":"radial-gradient(at 100% 100% in oklab, var(--nq-gradient-from) 0%, var(--nq-gradient-to) 100%) !important"}},[c.value==="pending"?(ee(),le("div",fa)):vs("",!0),Yt(" "+se(c.value==="pending"?H(o)("formContainer.sendingButton"):H(o)("formContainer.submitButtonDefault")),1)],8,ua)])],40,Xc))])}}}),pa={flex:""},ha=["placeholder"],ma={text:"neutral-700 f-sm","mt--8":"",flex:"~ items-center gap-8"},ga=At({__name:"IdeaForm",setup(e){const{t}=kt();return(n,s)=>(ee(),le(re,null,[F("label",pa,[F("textarea",{id:"description",name:"description",placeholder:H(t)("ideaForm.descriptionPlaceholder"),rows:"4",required:"","nq-input-box":""},null,8,ha)]),de(Di),F("div",ma,[s[0]||(s[0]=F("div",{"op-90":"","shrink-0":"","i-nimiq:info":""},null,-1)),F("p",null,se(H(t)("ideaForm.exampleHint")),1)])],64))}}),ba={key:0,"w-full":"",flex:"~ col"},_a={text:"24 center neutral lh-24","lh-none":"","font-bold":"","mb-12":""},va={grid:"~ rows-2 cols-2 gap-16",class:"grid-container","h-full":"","f-mt-lg":"","f-mb-md":""},ya=((e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n})(At({__name:"FeedbackWidget",props:{app:{},feedbackEndpoint:{},tags:{},initialForm:{},dark:{type:Boolean}},setup(e,{expose:t}){const n=e,{app:s,feedbackEndpoint:r,tags:i=[],dark:o=!1}=n,l=Re(),c=Ac(),{t:d}=kt(),{updateFiles:u}=Je(Dn);n.initialForm&&(l.value=n.initialForm),Mr(Oi,c);const p=oi(()=>{switch(l.value){case"bug":return Uc;case"idea":return ga;case"feedback":return Jc;default:return null}});function m(T){l.value=T,c.emit("form-selected",T)}function b(T){console.log("[Nimiq Feedback Widget] 📡 Notifying parent component..."),c.emit("form-submitted",{success:!0,data:T}),console.log("[Nimiq Feedback Widget] ✨ Event sent to host application")}function E({error:T,details:R}){c.emit("form-error",{success:!1,error:T,details:R})}return t({showFormGrid(){l.value=void 0,u([])},showForm(T){l.value=T},closeWidget(){l.value=void 0,u([])},goBack(){l.value=void 0,u([]),c.emit("go-back",void 0)},communication:c}),(T,R)=>(ee(),le("div",{style:xt({colorScheme:H(o)?"dark":"light"})},[de(Zl,{"enter-from-class":"op-0","enter-to-class":"op-100","leave-to-class":"op-0","enter-active-class":"transition-opacity duration-200","leave-active-class":"transition-opacity duration-200",mode:"out-in"},{default:is(()=>[l.value?(ee(),Gt(da,{key:1,type:l.value,app:H(s),"feedback-endpoint":H(r),tags:H(i),onFormSuccess:b,onFormError:E},{default:is(()=>[(ee(),Gt(Xo(p.value)))]),_:1},8,["type","app","feedback-endpoint","tags"])):(ee(),le("div",ba,[F("h3",_a,se(H(d)("feedbackWidget.title")),1),F("div",va,[F("button",{"col-span-2":"","nq-hoverable-red":"",onClick:R[0]||(R[0]=O=>m("bug"))},[R[3]||(R[3]=F("div",{"i-nimiq:exclamation":""},null,-1)),F("span",null,se(H(d)("feedbackWidget.bugReportButton")),1)]),F("button",{"nq-hoverable-green":"",onClick:R[1]||(R[1]=O=>m("idea"))},[R[4]||(R[4]=F("div",{"i-nimiq:leaf-2-filled":""},null,-1)),F("span",null,se(H(d)("feedbackWidget.ideaButton")),1)]),F("button",{"nq-hoverable-gold":"",onClick:R[2]||(R[2]=O=>m("feedback"))},[R[5]||(R[5]=F("div",{"i-nimiq:star":""},null,-1)),F("span",null,se(H(d)("feedbackWidget.feedbackButton")),1)])])]))]),_:1})],4))}}),[["__scopeId","data-v-3e19f25c"]]),xa={en:{attachmentUploader:{previewAlt:"Preview",deleteImageLabel:"Delete image",uploadHere:"Upload here",anyImageFormat:"Any image format",title:"Attachments"},bugForm:{descriptionPlaceholder:"Please, tell us how we can make your Nimiq experience better*",descriptionDefaultValue:"",emailLabel:"Email",emailPlaceholder:"If we need more information*",shareDebugInfoLabel:"Include technical details to help us fix this issue faster"},feedbackForm:{descriptionPlaceholder:"Please, tell us how we can make your Nimiq experience better*"},feedbackWidget:{title:"Send your feedback",bugReportButton:"Bug report",ideaButton:"Share your idea",feedbackButton:"Feedback",termsAndConditionsApply:"Terms and conditions apply"},formContainer:{titleBug:"Report a bug",titleIdea:"Share your idea",titleFeedback:"Give feedback",successMessage:"Submission successful! Please wait...",errorPrefix:"Error:",errorDetailsSummary:"Details",sendingButton:"Sending...",submitButtonDefault:"Submit Feedback",learnMore:"Learn more",privacyPolicyText:"about how we handle your data."},ideaForm:{descriptionPlaceholder:"Please, tell us how we can make your Nimiq experience better*",exampleHint:"For example a screenshot of a feature from another app"}},es:{attachmentUploader:{previewAlt:"Vista previa",deleteImageLabel:"Eliminar imagen",uploadHere:"Subir aquí",anyImageFormat:"Cualquier formato de imagen",title:"Adjuntos"},bugForm:{descriptionPlaceholder:"Por favor, cuéntanos cómo podemos mejorar tu experiencia con Nimiq*",descriptionDefaultValue:"¡Esto es una prueba, genial!",emailLabel:"Correo electrónico",emailPlaceholder:"Si necesitamos más información*",shareDebugInfoLabel:"Incluir detalles técnicos para ayudarnos a solucionar este problema más rápido"},feedbackForm:{descriptionPlaceholder:"Por favor, cuéntanos cómo podemos mejorar tu experiencia con Nimiq*"},feedbackWidget:{title:"Envía tu opinión",bugReportButton:"Reportar error",ideaButton:"Comparte tu idea",feedbackButton:"Comentarios",termsAndConditionsApply:"Términos y condiciones aplican"},formContainer:{titleBug:"Reportar un error",titleIdea:"Comparte tu idea",titleFeedback:"Dar comentarios",successMessage:"¡Envío exitoso! Por favor espera...",errorPrefix:"Error:",errorDetailsSummary:"Detalles",sendingButton:"Enviando...",submitButtonDefault:"Enviar comentarios",learnMore:"Más información",privacyPolicyText:"sobre cómo manejamos tus datos."},ideaForm:{descriptionPlaceholder:"Por favor, cuéntanos cómo podemos mejorar tu experiencia con Nimiq*",exampleHint:"Por ejemplo, una captura de pantalla de una función de otra aplicación"}},de:{attachmentUploader:{previewAlt:"Vorschau",deleteImageLabel:"Bild löschen",uploadHere:"Hier hochladen",anyImageFormat:"Beliebiges Bildformat",title:"Anhänge"},bugForm:{descriptionPlaceholder:"Bitte teile uns mit, wie wir deine Nimiq-Erfahrung verbessern können.*",descriptionDefaultValue:"",emailLabel:"E-Mail",emailPlaceholder:"Falls wir weitere Informationen benötigen*",shareDebugInfoLabel:"Technische Details einbeziehen, um dieses Problem schneller zu lösen"},feedbackForm:{descriptionPlaceholder:"Bitte teile uns mit, wie wir deine Nimiq-Erfahrung verbessern können.*"},feedbackWidget:{title:"Sende dein Feedback",bugReportButton:"Fehlerbericht senden",ideaButton:"Ideen vorschlagen",feedbackButton:"Feedback senden",termsAndConditionsApply:"Allgemeine Geschäftsbedingungen akzeptieren"},formContainer:{titleBug:"Einen Fehler melden",titleIdea:"Hast du eine Idee?",titleFeedback:"Feedback senden",successMessage:"Übermittlung erfolgreich! Bitte warten...",errorPrefix:"Fehler:",errorDetailsSummary:"Details",sendingButton:"Wird gesendet...",submitButtonDefault:"Feedback senden",learnMore:"Erfahre mehr",privacyPolicyText:"darüber, wie wir mit deinen Daten umgehen."},ideaForm:{descriptionPlaceholder:"Bitte teile uns mit, wie wir deine Nimiq-Erfahrung verbessern können.*",exampleHint:"Zum Beispiel einen Screenshot einer Funktion aus einer anderen App"}},pt:{attachmentUploader:{previewAlt:"Pré-visualização",deleteImageLabel:"Excluir imagem",uploadHere:"Enviar aqui",anyImageFormat:"Qualquer formato de imagem",title:"Anexos"},bugForm:{descriptionPlaceholder:"Por favor, nos conte como podemos melhorar sua experiência com Nimiq*",descriptionDefaultValue:"",emailLabel:"E-mail",emailPlaceholder:"Se precisarmos de mais informações*",shareDebugInfoLabel:"Incluir detalhes técnicos para nos ajudar a corrigir este problema mais rapidamente"},feedbackForm:{descriptionPlaceholder:"Por favor, nos conte como podemos melhorar sua experiência com Nimiq*"},feedbackWidget:{title:"Envie seu feedback",bugReportButton:"Relatar bug",ideaButton:"Compartilhe sua ideia",feedbackButton:"Feedback",termsAndConditionsApply:"Termos e condições se aplicam"},formContainer:{titleBug:"Relatar um bug",titleIdea:"Compartilhe sua ideia",titleFeedback:"Dar feedback",successMessage:"Envio bem-sucedido! Por favor, aguarde...",errorPrefix:"Erro:",errorDetailsSummary:"Detalhes",sendingButton:"Enviando...",submitButtonDefault:"Enviar Feedback",learnMore:"Saiba mais",privacyPolicyText:"sobre como tratamos seus dados."},ideaForm:{descriptionPlaceholder:"Por favor, nos conte como podemos melhorar sua experiência com Nimiq*",exampleHint:"Por exemplo, uma captura de tela de um recurso de outro aplicativo"}},fr:{attachmentUploader:{previewAlt:"Aperçu",deleteImageLabel:"Supprimer l'image",uploadHere:"Télécharger ici",anyImageFormat:"Tout format d'image",title:"Pièces jointes"},bugForm:{descriptionPlaceholder:"Veuillez nous dire comment nous pouvons améliorer votre expérience Nimiq*",descriptionDefaultValue:"",emailLabel:"E-mail",emailPlaceholder:"Si nous avons besoin de plus d'informations*",shareDebugInfoLabel:"Inclure les détails techniques pour nous aider à résoudre ce problème plus rapidement"},feedbackForm:{descriptionPlaceholder:"Veuillez nous dire comment nous pouvons améliorer votre expérience Nimiq*"},feedbackWidget:{title:"Envoyez vos commentaires",bugReportButton:"Signaler un bug",ideaButton:"Partagez votre idée",feedbackButton:"Commentaires",termsAndConditionsApply:"Les conditions générales s'appliquent"},formContainer:{titleBug:"Signaler un bug",titleIdea:"Partagez votre idée",titleFeedback:"Donner des commentaires",successMessage:"Soumission réussie ! Veuillez patienter...",errorPrefix:"Erreur :",errorDetailsSummary:"Détails",sendingButton:"Envoi en cours...",submitButtonDefault:"Envoyer les commentaires",learnMore:"En savoir plus",privacyPolicyText:"sur la façon dont nous traitons vos données."},ideaForm:{descriptionPlaceholder:"Veuillez nous dire comment nous pouvons améliorer votre expérience Nimiq*",exampleHint:"Par exemple, une capture d'écran d'une fonctionnalité d'une autre application"}}};function wa(e){return function(n,s){const r=n.split(".");let i=e;for(const o of r)if(i&&typeof i=="object"&&o in i)i=i[o];else return console.warn(`Translation key "${n}" not found`),n;return typeof i!="string"?(console.warn(`Translation key "${n}" does not resolve to a string`),n):s?i.replace(/\{(\w+)\}/g,(o,l)=>l in s?String(s[l]):o):i}}window.mountFeedbackWidget=(e,{app:t,lang:n="en",feedbackEndpoint:s,tags:r=[],initialForm:i,dark:o=!1})=>{const l=document.querySelector(e);if(!l)throw new Error(`Mount target ${e} not found`);console.log(`Mounting feedback widget for app: ${t}, lang: ${n}, tags: ${r}`,e);try{const c=xa[n],d={locale:n,messages:c,t:wa(c)},u=Re([]),p=Cc(ya,{app:t,feedbackEndpoint:s,tags:r,initialForm:i,dark:o}).provide(Ri,d).provide(Dn,{files:u,updateFiles:b=>u.value=b}),m=p.mount(l);return{showFormGrid(){m.showFormGrid()},showForm(b){m.showForm(b)},closeWidget(){m.closeWidget()},goBack(){m.goBack()},communication:m.communication,destroy(){try{p.unmount()}catch(b){console.error("Error destroying widget:",b)}}}}catch(c){return console.error("Error mounting feedback widget:",c),{showFormGrid(){},showForm(){},closeWidget(){},goBack(){},communication:{on(){},off(){},emit(){}},destroy(){}}}}}); +**/let Es;const di=typeof window<"u"&&window.trustedTypes;if(di)try{Es=di.createPolicy("vue",{createHTML:e=>e})}catch{}const pi=Es?e=>Es.createHTML(e):e=>e,Xl="http://www.w3.org/2000/svg",ec="http://www.w3.org/1998/Math/MathML",Ze=typeof document<"u"?document:null,hi=Ze&&Ze.createElement("template"),tc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Ze.createElementNS(Xl,e):t==="mathml"?Ze.createElementNS(ec,e):n?Ze.createElement(e,{is:n}):Ze.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Ze.createTextNode(e),createComment:e=>Ze.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ze.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{hi.innerHTML=pi(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=hi.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},lt="transition",tn="animation",nn=Symbol("_vtc"),mi={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},nc=ie({},vr,mi),sc=(e=>(e.displayName="Transition",e.props=nc,e))((e,{slots:t})=>Zl(Ko,rc(e),t)),mt=(e,t=[])=>{I(e)?e.forEach(n=>n(...t)):e&&e(...t)},gi=e=>e?I(e)?e.some(t=>t.length>1):e.length>1:!1;function rc(e){const t={};for(const E in e)E in mi||(t[E]=e[E]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:d=o,appearToClass:u=l,leaveFromClass:p=`${n}-leave-from`,leaveActiveClass:m=`${n}-leave-active`,leaveToClass:v=`${n}-leave-to`}=e,A=ic(r),F=A&&A[0],B=A&&A[1],{onBeforeEnter:k,onEnter:L,onEnterCancelled:D,onLeave:P,onLeaveCancelled:W,onBeforeAppear:le=k,onAppear:_e=L,onAppearCancelled:we=D}=t,N=(E,Q,me,et)=>{E._enterCancelled=et,gt(E,Q?u:l),gt(E,Q?d:o),me&&me()},z=(E,Q)=>{E._isLeaving=!1,gt(E,p),gt(E,v),gt(E,m),Q&&Q()},ne=E=>(Q,me)=>{const et=E?_e:L,ce=()=>N(Q,E,me);mt(et,[Q,ce]),bi(()=>{gt(Q,E?c:i),Qe(Q,E?u:l),gi(et)||vi(Q,s,F,ce)})};return ie(t,{onBeforeEnter(E){mt(k,[E]),Qe(E,i),Qe(E,o)},onBeforeAppear(E){mt(le,[E]),Qe(E,c),Qe(E,d)},onEnter:ne(!1),onAppear:ne(!0),onLeave(E,Q){E._isLeaving=!0;const me=()=>z(E,Q);Qe(E,p),E._enterCancelled?(Qe(E,m),xi()):(xi(),Qe(E,m)),bi(()=>{E._isLeaving&&(gt(E,p),Qe(E,v),gi(P)||vi(E,s,B,me))}),mt(P,[E,me])},onEnterCancelled(E){N(E,!1,void 0,!0),mt(D,[E])},onAppearCancelled(E){N(E,!0,void 0,!0),mt(we,[E])},onLeaveCancelled(E){z(E),mt(W,[E])}})}function ic(e){if(e==null)return null;if(G(e))return[As(e.enter),As(e.leave)];{const t=As(e);return[t,t]}}function As(e){return Yi(e)}function Qe(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[nn]||(e[nn]=new Set)).add(t)}function gt(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[nn];n&&(n.delete(t),n.size||(e[nn]=void 0))}function bi(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let oc=0;function vi(e,t,n,s){const r=e._endId=++oc,i=()=>{r===e._endId&&s()};if(n!=null)return setTimeout(i,n);const{type:o,timeout:l,propCount:c}=lc(e,t);if(!o)return s();const d=o+"end";let u=0;const p=()=>{e.removeEventListener(d,m),i()},m=v=>{v.target===e&&++u>=c&&p()};setTimeout(()=>{u(n[A]||"").split(", "),r=s(`${lt}Delay`),i=s(`${lt}Duration`),o=_i(r,i),l=s(`${tn}Delay`),c=s(`${tn}Duration`),d=_i(l,c);let u=null,p=0,m=0;t===lt?o>0&&(u=lt,p=o,m=i.length):t===tn?d>0&&(u=tn,p=d,m=c.length):(p=Math.max(o,d),u=p>0?o>d?lt:tn:null,m=u?u===lt?i.length:c.length:0);const v=u===lt&&/\b(?:transform|all)(?:,|$)/.test(s(`${lt}Property`).toString());return{type:u,timeout:p,propCount:m,hasTransform:v}}function _i(e,t){for(;e.lengthyi(n)+yi(e[s])))}function yi(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function xi(){return document.body.offsetHeight}function cc(e,t,n){const s=e[nn];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Si=Symbol("_vod"),ac=Symbol("_vsh"),uc=Symbol(""),fc=/(?:^|;)\s*display\s*:/;function dc(e,t,n){const s=e.style,r=X(n);let i=!1;if(n&&!r){if(t)if(X(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&Dn(s,l,"")}else for(const o in t)n[o]==null&&Dn(s,o,"");for(const o in n)o==="display"&&(i=!0),Dn(s,o,n[o])}else if(r){if(t!==n){const o=s[uc];o&&(n+=";"+o),s.cssText=n,i=fc.test(n)}}else t&&e.removeAttribute("style");Si in e&&(e[Si]=i?s.display:"",e[ac]&&(s.display="none"))}const wi=/\s*!important$/;function Dn(e,t,n){if(I(n))n.forEach(s=>Dn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=pc(e,t);wi.test(n)?e.setProperty(tt(s),n.replace(wi,""),"important"):e[s]=n}}const Ci=["Webkit","Moz","ms"],Ps={};function pc(e,t){const n=Ps[t];if(n)return n;let s=Fe(t);if(s!=="filter"&&s in e)return Ps[t]=s;s=un(s);for(let r=0;rIs||(bc.then(()=>Is=0),Is=Date.now());function _c(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;$e(yc(s,n.value),t,5,[s])};return n.value=e,n.attached=vc(),n}function yc(e,t){if(I(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Ii=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,xc=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?cc(e,s,o):t==="style"?dc(e,n,s):ln(t)?jn(t)||mc(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Sc(e,t,s,o))?(Ei(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Fi(e,t,s,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!X(s))?Ei(e,Fe(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Fi(e,t,s,o))};function Sc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Ii(t)&&R(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Ii(t)&&X(n)?!1:t in e}const Rt=e=>{const t=e.props["onUpdate:modelValue"]||!1;return I(t)?n=>fn(t,n):t};function wc(e){e.target.composing=!0}function ki(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Xe=Symbol("_assign"),ks={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[Xe]=Rt(r);const i=s||r.props&&r.props.type==="number";ct(e,t?"change":"input",o=>{if(o.target.composing)return;let l=e.value;n&&(l=l.trim()),i&&(l=Wn(l)),e[Xe](l)}),n&&ct(e,"change",()=>{e.value=e.value.trim()}),t||(ct(e,"compositionstart",wc),ct(e,"compositionend",ki),ct(e,"change",ki))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:i}},o){if(e[Xe]=Rt(o),e.composing)return;const l=(i||e.type==="number")&&!/^0\d/.test(e.value)?Wn(e.value):e.value,c=t??"";l!==c&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c))}},Mi={deep:!0,created(e,t,n){e[Xe]=Rt(n),ct(e,"change",()=>{const s=e._modelValue,r=Ri(e),i=e.checked,o=e[Xe];if(I(s)){const l=js(s,r),c=l!==-1;if(i&&!c)o(s.concat(r));else if(!i&&c){const d=[...s];d.splice(l,1),o(d)}}else if(cn(s)){const l=new Set(s);i?l.add(r):l.delete(r),o(l)}else o($i(e,i))})},mounted:Oi,beforeUpdate(e,t,n){e[Xe]=Rt(n),Oi(e,t,n)}};function Oi(e,{value:t,oldValue:n},s){e._modelValue=t;let r;if(I(t))r=js(t,s.props.value)>-1;else if(cn(t))r=t.has(s.props.value);else{if(t===n)return;r=wt(t,$i(e,!0))}e.checked!==r&&(e.checked=r)}const Cc={created(e,{value:t},n){e.checked=wt(t,n.props.value),e[Xe]=Rt(n),ct(e,"change",()=>{e[Xe](Ri(e))})},beforeUpdate(e,{value:t,oldValue:n},s){e[Xe]=Rt(s),t!==n&&(e.checked=wt(t,s.props.value))}};function Ri(e){return"_value"in e?e._value:e.value}function $i(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const Tc=["ctrl","shift","alt","meta"],Fc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Tc.some(n=>e[`${n}Key`]&&!t.includes(n))},Di=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=((r,...i)=>{for(let o=0;o{const t=Ac().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=kc(s);if(!r)return;const i=t._component;!R(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=n(r,!1,Ic(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t});function Ic(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function kc(e){return X(e)?document.querySelector(e):e}const Ni=Symbol("i18n");function $t(){return Me(Ni)}const Ln=Symbol("files"),Bi=Symbol("communication"),Nn=Symbol("form-validation");class Mc{constructor(){this.listeners={}}emit(t,n){this.listeners[t]?.forEach(s=>s(n))}on(t,n){this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(n)}off(t,n){if(!n){delete this.listeners[t];return}const s=this.listeners[t];if(s){const r=s.indexOf(n);r>-1&&s.splice(r,1)}}}function Oc(){return new Mc}const Rc={"mb-8":"",text:"12 neutral-800","nq-label":""},$c={grid:"~ gap-16 cols-[repeat(auto-fit,128px)]","w-full":""},Dc=["src","alt"],Lc=["aria-label","onClickCapture"],Nc={key:0,flex:"~ col justify-center items-center","rounded-4":"","size-128":"","aspect-square":"",outline:"1.5 neutral/15"},Bc={"font-semibold":"","mt-6":""},Vc={text:"f-xs neutral-800 center","mt-2":"","px-2":""},Vi=It({__name:"AttachmentUploader",props:{maxFiles:{default:5}},setup(e){const{files:t,updateFiles:n}=Me(Ln),s=ge([]),r=ge([]),{t:i}=$t(),o=ge();rt(t,d=>{d.length===0&&(s.value.forEach(u=>URL.revokeObjectURL(u)),s.value=[],r.value=[])},{deep:!0});function l(){if(!o.value)return;const d=[...t.value,...Array.from(o.value.files)].slice(0,e.maxFiles);n(d),s.value.forEach(u=>URL.revokeObjectURL(u)),s.value=[],r.value=[],t.value.forEach((u,p)=>{const m=URL.createObjectURL(u);s.value.push(m);const v=new Image;v.onload=()=>{const A=v.width/v.height;r.value[p]=A},v.src=m}),o.value.value=void 0}function c(d){URL.revokeObjectURL(s.value[d]),t.value.splice(d,1),s.value.splice(d,1),r.value.splice(d,1)}return(d,u)=>(te(),oe("label",{for:"attachments",class:at({"cursor-pointer":H(t).length===0}),group:"","w-full":""},[T("h3",Rc,ee(H(i)("attachmentUploader.title")),1),T("div",$c,[(te(!0),oe(re,null,us(s.value,(p,m)=>(te(),oe("div",{key:p,class:at(["stack outline-1.5 outline-neutral-200 rounded-4 size-128!",[r.value[m]<1?"":"aspect-square"]]),style:St(r.value[m]<1?{aspectRatio:r.value[m]}:{})},[T("img",{src:p,alt:H(i)("attachmentUploader.previewAlt",{number:m+1}),"h-full":"!","rounded-4":"","bg-neutral-100":"","max-h-128":"","object-contain":""},null,8,Dc),T("button",{type:"button","aria-label":H(i)("attachmentUploader.deleteImageLabel"),outline:"1.5 offset--1.5 white/8",stack:"","rounded-full":"","bg-white":"","size-24":"",shadow:"","self-start":"","right--12":"","top--12":"","justify-self-end":"",onClickCapture:Di(v=>c(m),["stop"])},[...u[0]||(u[0]=[T("div",{"i-nimiq:cross":"",text:"neutral 10"},null,-1)])],40,Lc)],6))),128)),H(t).length{r.description.value=i},{immediate:!0}),(i,o)=>(te(),oe(re,null,[T("label",jc,[Pt(T("textarea",{id:"description","onUpdate:modelValue":o[0]||(o[0]=l=>s.value=l),name:"description",placeholder:H(t)("bugForm.descriptionPlaceholder"),rows:"4",required:"","nq-input-box":"","focus-visible:outline-blue":""},null,8,Hc),[[ks,s.value]])]),Se(Vi),T("label",qc,[T("h3",Uc,ee(H(t)("bugForm.emailLabel")),1),T("input",{id:"email","w-auto":"",type:"email","nq-input-box":"",name:"email",placeholder:H(t)("bugForm.emailPlaceholder")},null,8,Wc)]),T("label",Kc,[T("span",zc,[Pt(T("input",{"onUpdate:modelValue":o[1]||(o[1]=l=>n.value=l),type:"checkbox",name:"shareDebugInfo","nq-switch":"","border-transparent":"!"},null,512),[[Mi,n.value]])]),T("span",Gc,ee(H(t)("bugForm.shareDebugInfoLabel")),1)])],64))}}),Yc={flex:"~ items-center justify-center gap-24",outline:"[&:has(:focus-visible)]:1.5 blue","mx-auto":"","rounded-4":"","w-max":"","f-p-sm":""},Zc=["id","value"],Qc=["for","data-state"],Xc={flex:"","f-mt-sm":""},ea=["placeholder"],ta=It({__name:"FeedbackForm",props:{modelValue:{default:0},modelModifiers:{}},emits:["update:modelValue"],setup(e){const t=Pl(e,"modelValue"),n=ge(""),{t:s}=$t(),r=Me(Nn);return r&&(rt(t,i=>{r.rating.value=i},{immediate:!0}),rt(n,i=>{r.description.value=i},{immediate:!0})),(i,o)=>(te(),oe(re,null,[T("div",Yc,[(te(),oe(re,null,us(5,l=>(te(),oe(re,{key:l},[Pt(T("input",{id:`rating-${l}`,"onUpdate:modelValue":o[0]||(o[0]=c=>t.value=c),class:"peer",type:"radio",name:"rating",value:l,"sr-only":""},null,8,Zc),[[Cc,t.value]]),T("label",{for:`rating-${l}`,"data-state":l<=t.value?"active":void 0,text:"neutral-300 data-[state=active]:gold hocus:gold [&:has(~_label:hover)]:gold",style:St(`--i: ${l}; --b:40ms`),delay:"[calc(25ms*var(--i))] group-hocus:[calc(var(--b)*(5-var(--i)))]","size-40":"","cursor-pointer":"","transition-colors":"","ease-out":"","i-nimiq:star":""},null,12,Qc)],64))),64))]),T("label",Xc,[Pt(T("textarea",{id:"description","onUpdate:modelValue":o[1]||(o[1]=l=>n.value=l),name:"description",placeholder:H(s)("feedbackForm.descriptionPlaceholder"),rows:"4",required:"","nq-input-box":""},null,8,ea),[[ks,n.value]])])],64))}}),na={flex:"~ col","h-full":""},sa={flex:"~ items-center gap-8","text-14":"","mb-16":"","h-max":"","w-full":"","text-balance":"","nq-label":""},ra={key:0,role:"alert"},ia=["data-app"],oa=["value"],la=["value"],ca=["value"],aa={flex:"~ items-start gap-8","f-text-sm":"","f-mt-sm":""},ua={"mt-1":"","shrink-0":"","h-1lh":""},fa={"text-neutral-800":"","select-none":""},da={"text-neutral-700":"","f-text-sm":"","f-mt-md":""},pa={href:"https://nimiq.com/terms/",target:"_blank","un-text-current":"",underline:""},ha={href:"https://nimiq.com/privacy-policy/",target:"_blank","un-text-current":"",underline:""},ma={key:0,role:"alert",text:"f-xs red-1100","font-semibold":""},ga={outline:"1.5 red-500","font-mono":"","font-normal":"","rounded-6":"","bg-red-400":"","f-p-2xs":""},ba={"mt-auto":"",flex:""},va=["disabled"],_a={key:0,"i-nimiq:spinner":""},ya=It({__name:"FormContainer",props:{type:{},app:{},feedbackEndpoint:{},tags:{default:()=>[]}},emits:["formSuccess","formError"],setup(e,{emit:t}){const n=t,{files:s}=Me(Ln),r=Me(Bi),i=ge(!1),o=ge(""),l=ge(0);ms(Nn,{description:o,rating:l});const{t:c}=$t(),d=Fs(()=>{const k=o.value.trim().length>0,L=e.type==="feedback"?l.value>0:!0,D=i.value;return k&&L&&D}),u=ge(),p=ge("idle"),m=ge(),v={bug:"formContainer.titleBug",idea:"formContainer.titleIdea",feedback:"formContainer.titleFeedback"},A={bug:"i-nimiq:exclamation",idea:"i-nimiq:leaf-2-filled",feedback:"i-nimiq:star"},F={bug:"bg-gradient-red",idea:"bg-gradient-green",feedback:"bg-gradient-gold"};async function B(k){console.log("[Nimiq Feedback Widget] 📤 Starting form submission..."),p.value="pending";const L=k.target,D=new FormData(L);(e.type==="bug"||e.type==="idea")&&(console.log("Submitting feedback:",{type:D.get("type"),app:D.get("app"),attachments:s.value}),D.delete("attachments"),console.log("Files to be attached:",s.value),Array.from(s.value).forEach(W=>D.append("attachments",W)),console.log("Form data after appending files:",Array.from(D.entries()))),r?.emit("before-submit",{formData:D,type:e.type,app:e.app});const P=await fetch(e.feedbackEndpoint,{method:"POST",body:D}).catch(W=>(p.value="error",W));if(!P.ok){console.log(`[Nimiq Feedback Widget] ❌ Form submission failed: ${P.status} ${P.statusText}`),p.value="error",u.value=await P.json(),n("formError",{error:u.value.message,details:u.value});return}m.value=await P.json(),p.value="success",console.log("[Nimiq Feedback Widget] ✅ Form submitted successfully"),n("formSuccess",m.value)}return(k,L)=>(te(),oe("div",na,[T("h2",sa,[T("div",{class:at(F[k.type]),stack:"","rounded-3":"","shrink-0":"","size-24":"",style:{"box-shadow":"0px 4px 16px 0px rgba(0, 0, 0, 0.07), 0px 1.5px 3px 0px rgba(0, 0, 0, 0.05), 0px 0.337px 2px 0px rgba(0, 0, 0, 0.03)"}},[T("div",{class:at(A[k.type]),"text-white":""},null,2)],2),Ot(" "+ee(H(c)(v[k.type])),1)]),p.value==="success"?(te(),oe("div",ra,[T("p",null,ee(H(c)("formContainer.successMessage")),1)])):(te(),oe("form",{key:1,flex:"~ col gap-16","px-1.5":"","h-full":"","data-app":k.app,onSubmit:Di(B,["prevent"])},[T("input",{type:"text",name:"type",value:k.type,"sr-only":""},null,8,oa),T("input",{type:"text",name:"app",value:k.app,"sr-only":""},null,8,la),T("input",{type:"text",name:"tags",value:k.tags.join(","),"sr-only":""},null,8,ca),ol(k.$slots,"default"),T("label",aa,[T("span",ua,[Pt(T("input",{"onUpdate:modelValue":L[0]||(L[0]=D=>i.value=D),type:"checkbox",name:"acceptTerms","border-transparent":"!",required:"","nq-switch":""},null,512),[[Mi,i.value]])]),T("span",fa,[Ot(ee(H(c)("formContainer.consentTermsAndFeedback"))+" ",1),L[1]||(L[1]=T("span",{"text-orange":""},"*",-1))])]),T("p",da,[T("a",pa,ee(H(c)("formContainer.readFullTerms")),1),L[2]||(L[2]=T("span",{"mx-8":""},"·",-1)),T("a",ha,ee(H(c)("formContainer.learnMore")),1),Ot(" "+ee(H(c)("formContainer.privacyPolicyText")),1)]),p.value==="error"?(te(),oe("div",ma,[T("p",null,[T("strong",null,ee(H(c)("formContainer.errorPrefix")),1),Ot(" "+ee(u.value?.message),1)]),(te(!0),oe(re,null,us(u.value.issues,D=>(te(),oe("ul",{key:D,"list-disc":"","f-px-xs":""},[T("li",null,ee(D),1)]))),128)),T("details",null,[T("summary",null,ee(H(c)("formContainer.errorDetailsSummary")),1),T("pre",ga,ee(u.value),1)])])):Ss("",!0),T("div",ba,[T("button",{type:"submit",disabled:!d.value||p.value==="pending","mx-0":"","mb-0":"","w-full":"","nq-pill-xl":"","nq-pill-blue":"","disabled:op-60":"",style:{"background-image":"radial-gradient(at 100% 100% in oklab, var(--nq-gradient-from) 0%, var(--nq-gradient-to) 100%) !important"}},[p.value==="pending"?(te(),oe("div",_a)):Ss("",!0),Ot(" "+ee(p.value==="pending"?H(c)("formContainer.sendingButton"):H(c)("formContainer.submitButtonDefault")),1)],8,va)])],40,ia))]))}}),xa={flex:""},Sa=["placeholder"],wa={text:"neutral-700 f-sm","mt--8":"",flex:"~ items-center gap-8"},Ca=It({__name:"IdeaForm",setup(e){const{t}=$t(),n=ge(""),s=Me(Nn);return s&&rt(n,r=>{s.description.value=r},{immediate:!0}),(r,i)=>(te(),oe(re,null,[T("label",xa,[Pt(T("textarea",{id:"description","onUpdate:modelValue":i[0]||(i[0]=o=>n.value=o),name:"description",placeholder:H(t)("ideaForm.descriptionPlaceholder"),rows:"4",required:"","nq-input-box":""},null,8,Sa),[[ks,n.value]])]),Se(Vi),T("div",wa,[i[1]||(i[1]=T("span",{"shrink-0":"","h-1lh":""},[T("div",{"op-90":"","i-nimiq:info":""})],-1)),T("p",null,ee(H(t)("ideaForm.exampleHint")),1)])],64))}}),Ta={key:0,"w-full":"",flex:"~ col"},Fa={text:"24 center neutral lh-24","lh-none":"","font-bold":"","mb-12":""},Ea={grid:"~ rows-2 cols-2 gap-16",class:"grid-container","h-full":"","f-mt-lg":"","f-mb-md":""},Aa=((e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n})(It({__name:"FeedbackWidget",props:{app:{},feedbackEndpoint:{},tags:{},initialForm:{},dark:{type:Boolean}},setup(e,{expose:t}){const n=e,{app:s,feedbackEndpoint:r,tags:i=[],dark:o=!1}=n,l=ge(),c=Oc(),{t:d}=$t(),{updateFiles:u}=Me(Ln);n.initialForm&&(l.value=n.initialForm),ms(Bi,c);const p=Fs(()=>{switch(l.value){case"bug":return Jc;case"idea":return Ca;case"feedback":return ta;default:return null}});function m(F){l.value=F,c.emit("form-selected",F)}function v(F){console.log("[Nimiq Feedback Widget] 📡 Notifying parent component..."),c.emit("form-submitted",{success:!0,data:F}),console.log("[Nimiq Feedback Widget] ✨ Event sent to host application")}function A({error:F,details:B}){c.emit("form-error",{success:!1,error:F,details:B})}return t({showFormGrid(){l.value=void 0,u([])},showForm(F){l.value=F},closeWidget(){l.value=void 0,u([])},goBack(){l.value=void 0,u([]),c.emit("go-back",void 0)},communication:c}),(F,B)=>(te(),oe("div",{style:St({colorScheme:H(o)?"dark":"light"})},[Se(sc,{"enter-from-class":"op-0","enter-to-class":"op-100","leave-to-class":"op-0","enter-active-class":"transition-opacity duration-200","leave-active-class":"transition-opacity duration-200",mode:"out-in"},{default:ls(()=>[l.value?(te(),Zt(ya,{key:1,type:l.value,app:H(s),"feedback-endpoint":H(r),tags:H(i),onFormSuccess:v,onFormError:A},{default:ls(()=>[(te(),Zt(rl(p.value)))]),_:1},8,["type","app","feedback-endpoint","tags"])):(te(),oe("div",Ta,[T("h3",Fa,ee(H(d)("feedbackWidget.title")),1),T("div",Ea,[T("button",{"text-white":"","col-span-2":"","nq-hoverable-red":"",flex:"~ col justify-center items-center gap-12",onClick:B[0]||(B[0]=k=>m("bug"))},[B[3]||(B[3]=T("div",{"f-text-2xl":"","i-nimiq:exclamation":""},null,-1)),T("span",null,ee(H(d)("feedbackWidget.bugReportButton")),1)]),T("button",{"text-white":"","nq-hoverable-green":"",flex:"~ col justify-center items-center gap-12",onClick:B[1]||(B[1]=k=>m("idea"))},[B[4]||(B[4]=T("div",{"f-text-2xl":"","i-nimiq:leaf-2-filled":""},null,-1)),T("span",null,ee(H(d)("feedbackWidget.ideaButton")),1)]),T("button",{"text-white":"","nq-hoverable-gold":"",flex:"~ col justify-center items-center gap-12",onClick:B[2]||(B[2]=k=>m("feedback"))},[B[5]||(B[5]=T("div",{"f-text-2xl":"","i-nimiq:star":""},null,-1)),T("span",null,ee(H(d)("feedbackWidget.feedbackButton")),1)])])]))]),_:1})],4))}}),[["__scopeId","data-v-f92a7dce"]]),Pa={en:{attachmentUploader:{previewAlt:"Preview",deleteImageLabel:"Delete image",uploadHere:"Upload here",anyImageFormat:"Any image format",title:"Attachments"},bugForm:{descriptionPlaceholder:"Please, tell us how we can make your Nimiq experience better*",descriptionDefaultValue:"",emailLabel:"Email",emailPlaceholder:"If we need more information",shareDebugInfoLabel:"Include technical details to help us fix this issue faster"},feedbackForm:{descriptionPlaceholder:"Please, tell us how we can make your Nimiq experience better*"},feedbackWidget:{title:"Send your feedback",bugReportButton:"Bug report",ideaButton:"Share your idea",feedbackButton:"Feedback",termsAndConditionsApply:"Terms and conditions apply"},formContainer:{titleBug:"Report a bug",titleIdea:"Share your idea",titleFeedback:"Give feedback",successMessage:"Submission successful! Please wait...",errorPrefix:"Error:",errorDetailsSummary:"Details",sendingButton:"Sending...",submitButtonDefault:"Submit Feedback",consentTermsAndFeedback:"I agree to the Terms of Service and use of my feedback to improve Nimiq products",readFullTerms:"Read full Terms",learnMore:"Learn more",privacyPolicyText:"about how we handle your data."},ideaForm:{descriptionPlaceholder:"Please, tell us how we can make your Nimiq experience better*",exampleHint:"For example a screenshot of a feature from another app"}},es:{attachmentUploader:{previewAlt:"Vista previa",deleteImageLabel:"Eliminar imagen",uploadHere:"Subir aquí",anyImageFormat:"Cualquier formato de imagen",title:"Adjuntos"},bugForm:{descriptionPlaceholder:"Por favor, cuéntanos cómo podemos mejorar tu experiencia con Nimiq*",descriptionDefaultValue:"¡Esto es una prueba, genial!",emailLabel:"Correo electrónico",emailPlaceholder:"Si necesitamos más información",shareDebugInfoLabel:"Incluir detalles técnicos para ayudarnos a solucionar este problema más rápido"},feedbackForm:{descriptionPlaceholder:"Por favor, cuéntanos cómo podemos mejorar tu experiencia con Nimiq*"},feedbackWidget:{title:"Envía tu opinión",bugReportButton:"Reportar error",ideaButton:"Comparte tu idea",feedbackButton:"Comentarios",termsAndConditionsApply:"Términos y condiciones aplican"},formContainer:{titleBug:"Reportar un error",titleIdea:"Comparte tu idea",titleFeedback:"Dar comentarios",successMessage:"¡Envío exitoso! Por favor espera...",errorPrefix:"Error:",errorDetailsSummary:"Detalles",sendingButton:"Enviando...",submitButtonDefault:"Enviar comentarios",consentTermsAndFeedback:"Acepto los Términos de Servicio y el uso de mis comentarios para mejorar los productos de Nimiq",readFullTerms:"Leer Términos completos",learnMore:"Más información",privacyPolicyText:"sobre cómo manejamos tus datos."},ideaForm:{descriptionPlaceholder:"Por favor, cuéntanos cómo podemos mejorar tu experiencia con Nimiq*",exampleHint:"Por ejemplo, una captura de pantalla de una función de otra aplicación"}},de:{attachmentUploader:{previewAlt:"Vorschau",deleteImageLabel:"Bild löschen",uploadHere:"Hier hochladen",anyImageFormat:"Beliebiges Bildformat",title:"Anhänge"},bugForm:{descriptionPlaceholder:"Bitte teile uns mit, wie wir deine Nimiq-Erfahrung verbessern können.*",descriptionDefaultValue:"",emailLabel:"E-Mail",emailPlaceholder:"Falls wir weitere Informationen benötigen",shareDebugInfoLabel:"Technische Details einbeziehen, um dieses Problem schneller zu lösen"},feedbackForm:{descriptionPlaceholder:"Bitte teile uns mit, wie wir deine Nimiq-Erfahrung verbessern können.*"},feedbackWidget:{title:"Sende dein Feedback",bugReportButton:"Fehlerbericht senden",ideaButton:"Ideen vorschlagen",feedbackButton:"Feedback senden",termsAndConditionsApply:"Allgemeine Geschäftsbedingungen akzeptieren"},formContainer:{titleBug:"Einen Fehler melden",titleIdea:"Hast du eine Idee?",titleFeedback:"Feedback senden",successMessage:"Übermittlung erfolgreich! Bitte warten...",errorPrefix:"Fehler:",errorDetailsSummary:"Details",sendingButton:"Wird gesendet...",submitButtonDefault:"Feedback senden",consentTermsAndFeedback:"Ich stimme den Nutzungsbedingungen und der Verwendung meines Feedbacks zur Verbesserung der Nimiq-Produkte zu",readFullTerms:"Vollständige Bedingungen lesen",learnMore:"Erfahre mehr",privacyPolicyText:"darüber, wie wir mit deinen Daten umgehen."},ideaForm:{descriptionPlaceholder:"Bitte teile uns mit, wie wir deine Nimiq-Erfahrung verbessern können.*",exampleHint:"Zum Beispiel einen Screenshot einer Funktion aus einer anderen App"}},pt:{attachmentUploader:{previewAlt:"Pré-visualização",deleteImageLabel:"Excluir imagem",uploadHere:"Enviar aqui",anyImageFormat:"Qualquer formato de imagem",title:"Anexos"},bugForm:{descriptionPlaceholder:"Por favor, nos conte como podemos melhorar sua experiência com Nimiq*",descriptionDefaultValue:"",emailLabel:"E-mail",emailPlaceholder:"Se precisarmos de mais informações",shareDebugInfoLabel:"Incluir detalhes técnicos para nos ajudar a corrigir este problema mais rapidamente"},feedbackForm:{descriptionPlaceholder:"Por favor, nos conte como podemos melhorar sua experiência com Nimiq*"},feedbackWidget:{title:"Envie seu feedback",bugReportButton:"Relatar bug",ideaButton:"Compartilhe sua ideia",feedbackButton:"Feedback",termsAndConditionsApply:"Termos e condições se aplicam"},formContainer:{titleBug:"Relatar um bug",titleIdea:"Compartilhe sua ideia",titleFeedback:"Dar feedback",successMessage:"Envio bem-sucedido! Por favor, aguarde...",errorPrefix:"Erro:",errorDetailsSummary:"Detalhes",sendingButton:"Enviando...",submitButtonDefault:"Enviar Feedback",consentTermsAndFeedback:"Concordo com os Termos de Serviço e uso do meu feedback para melhorar os produtos Nimiq",readFullTerms:"Ler Termos completos",learnMore:"Saiba mais",privacyPolicyText:"sobre como tratamos seus dados."},ideaForm:{descriptionPlaceholder:"Por favor, nos conte como podemos melhorar sua experiência com Nimiq*",exampleHint:"Por exemplo, uma captura de tela de um recurso de outro aplicativo"}},fr:{attachmentUploader:{previewAlt:"Aperçu",deleteImageLabel:"Supprimer l'image",uploadHere:"Télécharger ici",anyImageFormat:"Tout format d'image",title:"Pièces jointes"},bugForm:{descriptionPlaceholder:"Veuillez nous dire comment nous pouvons améliorer votre expérience Nimiq*",descriptionDefaultValue:"",emailLabel:"E-mail",emailPlaceholder:"Si nous avons besoin de plus d'informations",shareDebugInfoLabel:"Inclure les détails techniques pour nous aider à résoudre ce problème plus rapidement"},feedbackForm:{descriptionPlaceholder:"Veuillez nous dire comment nous pouvons améliorer votre expérience Nimiq*"},feedbackWidget:{title:"Envoyez vos commentaires",bugReportButton:"Signaler un bug",ideaButton:"Partagez votre idée",feedbackButton:"Commentaires",termsAndConditionsApply:"Les conditions générales s'appliquent"},formContainer:{titleBug:"Signaler un bug",titleIdea:"Partagez votre idée",titleFeedback:"Donner des commentaires",successMessage:"Soumission réussie ! Veuillez patienter...",errorPrefix:"Erreur :",errorDetailsSummary:"Détails",sendingButton:"Envoi en cours...",submitButtonDefault:"Envoyer les commentaires",consentTermsAndFeedback:"J'accepte les Conditions de Service et l'utilisation de mes commentaires pour améliorer les produits Nimiq",readFullTerms:"Lire les Conditions complètes",learnMore:"En savoir plus",privacyPolicyText:"sur la façon dont nous traitons vos données."},ideaForm:{descriptionPlaceholder:"Veuillez nous dire comment nous pouvons améliorer votre expérience Nimiq*",exampleHint:"Par exemple, une capture d'écran d'une fonctionnalité d'une autre application"}}};function Ia(e){return function(n,s){const r=n.split(".");let i=e;for(const o of r)if(i&&typeof i=="object"&&o in i)i=i[o];else return console.warn(`Translation key "${n}" not found`),n;return typeof i!="string"?(console.warn(`Translation key "${n}" does not resolve to a string`),n):s?i.replace(/\{(\w+)\}/g,(o,l)=>l in s?String(s[l]):o):i}}window.mountFeedbackWidget=(e,{app:t,lang:n="en",feedbackEndpoint:s,tags:r=[],initialForm:i,dark:o=!1})=>{const l=document.querySelector(e);if(!l)throw new Error(`Mount target ${e} not found`);console.log(`Mounting feedback widget for app: ${t}, lang: ${n}, tags: ${r}`,e);try{const c=Pa[n],d={locale:n,messages:c,t:Ia(c)},u=ge([]),p=Pc(Aa,{app:t,feedbackEndpoint:s,tags:r,initialForm:i,dark:o}).provide(Ni,d).provide(Ln,{files:u,updateFiles:v=>u.value=v}),m=p.mount(l);return{showFormGrid(){m.showFormGrid()},showForm(v){m.showForm(v)},closeWidget(){m.closeWidget()},goBack(){m.goBack()},communication:m.communication,destroy(){try{p.unmount()}catch(v){console.error("Error destroying widget:",v)}}}}catch(c){return console.error("Error mounting feedback widget:",c),{showFormGrid(){},showForm(){},closeWidget(){},goBack(){},communication:{on(){},off(){},emit(){}},destroy(){}}}}})); diff --git a/src/components/CustomTopBar.astro b/src/components/CustomTopBar.astro index c24c425..1165689 100644 --- a/src/components/CustomTopBar.astro +++ b/src/components/CustomTopBar.astro @@ -44,13 +44,7 @@ const version = import.meta.env.PACKAGE_VERSION || '0.0.2' // Add version info to logo title on page load document.addEventListener('DOMContentLoaded', () => { // Find the logo element (assuming it's the first child of the logo slot) - const logoSlot = document.querySelector('nav [slot="logo"]') || - document.querySelector('nav a[href="/"]') || - document.querySelector('nav img') || - document.querySelector('nav svg') - - if (logoSlot) { - logoSlot.title = `Version ${version}` - } + const logoSlot = document.querySelector('nav [slot="logo"]') || document.querySelector('nav a[href="/"]') || document.querySelector('nav img') || document.querySelector('nav svg') + if (logoSlot) logoSlot.title = `Version ${version}` }) diff --git a/src/content/tutorial/5-polygon-basics/1-introduction/_files/index.js b/src/content/tutorial/5-polygon-basics/1-introduction/_files/index.js new file mode 100644 index 0000000..331b3a3 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/1-introduction/_files/index.js @@ -0,0 +1,173 @@ +import { ethers } from 'ethers' +import { createWalletFromPassword } from './lib/wallet.js' + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const USDC_ADDRESS = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' +const USDC_ABI = [ + 'function transfer(address to, uint256 amount) returns (bool)', + 'function balanceOf(address account) view returns (uint256)', + 'function decimals() view returns (uint8)', +] + +// 🔐 WALLET SETUP: Change this to your own unique password! +// Same password = same wallet address every time (great for tutorials) +const WALLET_PASSWORD = 'change_me_to_something_unique_like_pizza_unicorn_2024' + +// 📤 RECIPIENT: This is a Nimiq-controlled account that collects tutorial demo transfers +// If you accidentally send large amounts or want your funds back, contact us and we'll return them! +// +// 💡 Want to send to yourself instead? Uncomment these lines to create a second wallet: +// const recipientWallet = createWalletFromPassword('my_second_wallet_password_banana_2024') +// const RECIPIENT = recipientWallet.address +// This way you control both wallets and can recover any funds sent! +// +// Or create your own wallet with a standard 24-word mnemonic (see lib/wallet.js) +const RECIPIENT = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +async function main() { + console.log('🚀 Polygon Basics - Complete Demo\n') + console.log('This demo shows all concepts from lessons 2-4:\n') + + // ========== LESSON 2: WALLET SETUP & FAUCETS ========== + console.log('📚 LESSON 2: Wallet Setup & Faucets') + console.log('─'.repeat(50)) + + const provider = new ethers.providers.JsonRpcProvider(RPC_URL) + + // Create wallet from password (see lib/wallet.js for alternatives) + const wallet = createWalletFromPassword(WALLET_PASSWORD).connect(provider) + console.log('✅ Wallet created from password') + + if (WALLET_PASSWORD === 'change_me_to_something_unique_like_pizza_unicorn_2024') { + console.log('⚠️ Using default password! Change WALLET_PASSWORD to your own unique string.') + } + + console.log('📍 Address:', wallet.address) + console.log('🔗 View on explorer:', `https://amoy.polygonscan.com/address/${wallet.address}`) + + // Check balances + const polBalance = await provider.getBalance(wallet.address) + console.log('💰 POL Balance:', ethers.utils.formatEther(polBalance), 'POL') + + const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, wallet) + const usdcBalance = await usdc.balanceOf(wallet.address) + const decimals = await usdc.decimals() + console.log('💵 USDC Balance:', ethers.utils.formatUnits(usdcBalance, decimals), 'USDC') + + // Convert to 24-word mnemonic so you can import into any wallet + const mnemonic = ethers.Wallet.fromMnemonic( + ethers.utils.entropyToMnemonic(ethers.utils.hexZeroPad(wallet.privateKey, 32)), + ).mnemonic.phrase + console.log('\n📝 Mnemonic (24 words):', mnemonic) + console.log('💡 You can import this mnemonic into any wallet to check your balances:') + console.log(' • Nimiq Testnet Wallet: https://wallet.nimiq-testnet.com/ (supports Amoy USDC)') + console.log(' • Note: Nimiq Wallet does not support POL, only USDC/USDT') + console.log(' • Or use MetaMask, Trust Wallet, etc.\n') + + if (polBalance.eq(0)) { + console.log('\n⚠️ No POL found! Get free tokens from:') + console.log(' POL & USDC: https://faucet.polygon.technology/') + console.log(' USDC also: https://faucet.circle.com/') + console.log(' Then run this demo again!\n') + return + } + + // ========== LESSON 3: SENDING POL ========== + console.log('\n📚 LESSON 3: Sending POL Transactions') + console.log('─'.repeat(50)) + + const POL_AMOUNT = '0.0001' // Minimal amount for demo + + console.log('📤 Sending', POL_AMOUNT, 'POL to', RECIPIENT) + + try { + // Get current gas price and ensure minimum for Polygon + const feeData = await provider.getFeeData() + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas.lt(ethers.utils.parseUnits('30', 'gwei')) + ? ethers.utils.parseUnits('30', 'gwei') + : feeData.maxPriorityFeePerGas + + // Ensure maxFeePerGas is higher than maxPriorityFeePerGas + const maxFeePerGas = feeData.maxFeePerGas.lt(maxPriorityFeePerGas) + ? maxPriorityFeePerGas.mul(2) + : feeData.maxFeePerGas + + const tx = await wallet.sendTransaction({ + to: RECIPIENT, + value: ethers.utils.parseEther(POL_AMOUNT), + maxPriorityFeePerGas, + maxFeePerGas, + }) + console.log('⏳ Transaction hash:', tx.hash) + + const receipt = await tx.wait() + console.log('✅ Confirmed in block:', receipt.blockNumber) + console.log('⛽ Gas used:', receipt.gasUsed.toString()) + console.log('🔗 View:', `https://amoy.polygonscan.com/tx/${tx.hash}`) + + // Show balance change + const newPolBalance = await provider.getBalance(wallet.address) + const spent = polBalance.sub(newPolBalance) + console.log('📊 Total spent:', ethers.utils.formatEther(spent), 'POL (including gas)') + } + catch (error) { + console.log('❌ Failed:', error.message) + } + + // ========== LESSON 4: ERC20 & USDC ========== + console.log('\n📚 LESSON 4: ERC20 Tokens & USDC Transfers') + console.log('─'.repeat(50)) + + if (usdcBalance.eq(0)) { + console.log('⚠️ No USDC found! Get free USDC from:') + console.log(' https://faucet.polygon.technology/') + console.log(' https://faucet.circle.com/') + console.log(' Then try this section again!\n') + return + } + + const USDC_AMOUNT = '0.01' // Minimal amount for demo + console.log('📤 Sending', USDC_AMOUNT, 'USDC to', RECIPIENT) + + try { + // Get current gas price and ensure minimum for Polygon + const feeData = await provider.getFeeData() + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas.lt(ethers.utils.parseUnits('30', 'gwei')) + ? ethers.utils.parseUnits('30', 'gwei') + : feeData.maxPriorityFeePerGas + + // Ensure maxFeePerGas is higher than maxPriorityFeePerGas + const maxFeePerGas = feeData.maxFeePerGas.lt(maxPriorityFeePerGas) + ? maxPriorityFeePerGas.mul(2) + : feeData.maxFeePerGas + + const amountInBaseUnits = ethers.utils.parseUnits(USDC_AMOUNT, decimals) + const tx = await usdc.transfer(RECIPIENT, amountInBaseUnits, { + maxPriorityFeePerGas, + maxFeePerGas, + }) + console.log('⏳ Transaction hash:', tx.hash) + + const receipt = await tx.wait() + console.log('✅ Confirmed in block:', receipt.blockNumber) + console.log('🔗 View:', `https://amoy.polygonscan.com/tx/${tx.hash}`) + + // Show balance change + const newUsdcBalance = await usdc.balanceOf(wallet.address) + console.log('📊 New USDC balance:', ethers.utils.formatUnits(newUsdcBalance, decimals), 'USDC') + + const recipientBalance = await usdc.balanceOf(RECIPIENT) + console.log('📊 Recipient USDC:', ethers.utils.formatUnits(recipientBalance, decimals), 'USDC') + + // Show POL used for gas (still needed for ERC20!) + const finalPolBalance = await provider.getBalance(wallet.address) + console.log('⛽ POL balance:', ethers.utils.formatEther(finalPolBalance), 'POL (gas paid in POL!)') + } + catch (error) { + console.log('❌ Failed:', error.message) + } + + console.log('\n🎉 Demo complete! Now try lessons 2-4 step by step.') +} + +main().catch(console.error) diff --git a/src/content/tutorial/5-polygon-basics/1-introduction/_files/lib/wallet.js b/src/content/tutorial/5-polygon-basics/1-introduction/_files/lib/wallet.js new file mode 100644 index 0000000..5cc965c --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/1-introduction/_files/lib/wallet.js @@ -0,0 +1,44 @@ +import { ethers } from 'ethers' + +/** + * Creates a wallet from a password string. + * + * WHY USE THIS? + * - You can recreate the same wallet anytime by using the same password + * - No need to save private keys in files or worry about losing them + * - Just remember your password and you can access your wallet from any code + * + * HOW IT WORKS: + * - Your password is hashed (keccak256) to create a deterministic private key + * - Same password = same private key = same wallet address every time + * + * SECURITY NOTE: + * - This is PERFECT for testnets (learning, experiments) + * - For mainnet with real money, use a hardware wallet or proper mnemonic phrase + */ +export function createWalletFromPassword(password) { + // Hash the password to get a deterministic private key + const privateKey = ethers.utils.id(password) + const wallet = new ethers.Wallet(privateKey) + + return wallet +} + +/** + * Alternative wallet creation methods: + * + * 1. FROM MNEMONIC (12/24 word phrase): + * const mnemonic = "word1 word2 word3 ... word12" + * const wallet = ethers.Wallet.fromMnemonic(mnemonic) + * + * 2. FROM PRIVATE KEY (hex string): + * const privateKey = "0x1234567890abcdef..." + * const wallet = new ethers.Wallet(privateKey) + * + * 3. RANDOM WALLET (new every time): + * const wallet = ethers.Wallet.createRandom() + * console.log('Save this mnemonic:', wallet.mnemonic.phrase) + * + * 4. FROM PASSWORD (this method - best for tutorials): + * const wallet = createWalletFromPassword("my_unique_password_123") + */ diff --git a/src/content/tutorial/5-polygon-basics/1-introduction/_files/package.json b/src/content/tutorial/5-polygon-basics/1-introduction/_files/package.json new file mode 100644 index 0000000..e13dae1 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/1-introduction/_files/package.json @@ -0,0 +1 @@ +{ "name": "polygon-basics-demo", "type": "module", "version": "1.0.0", "scripts": { "demo": "node --watch index.js" }, "dependencies": { "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/5-polygon-basics/1-introduction/_solution/index.js b/src/content/tutorial/5-polygon-basics/1-introduction/_solution/index.js new file mode 100644 index 0000000..331b3a3 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/1-introduction/_solution/index.js @@ -0,0 +1,173 @@ +import { ethers } from 'ethers' +import { createWalletFromPassword } from './lib/wallet.js' + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const USDC_ADDRESS = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' +const USDC_ABI = [ + 'function transfer(address to, uint256 amount) returns (bool)', + 'function balanceOf(address account) view returns (uint256)', + 'function decimals() view returns (uint8)', +] + +// 🔐 WALLET SETUP: Change this to your own unique password! +// Same password = same wallet address every time (great for tutorials) +const WALLET_PASSWORD = 'change_me_to_something_unique_like_pizza_unicorn_2024' + +// 📤 RECIPIENT: This is a Nimiq-controlled account that collects tutorial demo transfers +// If you accidentally send large amounts or want your funds back, contact us and we'll return them! +// +// 💡 Want to send to yourself instead? Uncomment these lines to create a second wallet: +// const recipientWallet = createWalletFromPassword('my_second_wallet_password_banana_2024') +// const RECIPIENT = recipientWallet.address +// This way you control both wallets and can recover any funds sent! +// +// Or create your own wallet with a standard 24-word mnemonic (see lib/wallet.js) +const RECIPIENT = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +async function main() { + console.log('🚀 Polygon Basics - Complete Demo\n') + console.log('This demo shows all concepts from lessons 2-4:\n') + + // ========== LESSON 2: WALLET SETUP & FAUCETS ========== + console.log('📚 LESSON 2: Wallet Setup & Faucets') + console.log('─'.repeat(50)) + + const provider = new ethers.providers.JsonRpcProvider(RPC_URL) + + // Create wallet from password (see lib/wallet.js for alternatives) + const wallet = createWalletFromPassword(WALLET_PASSWORD).connect(provider) + console.log('✅ Wallet created from password') + + if (WALLET_PASSWORD === 'change_me_to_something_unique_like_pizza_unicorn_2024') { + console.log('⚠️ Using default password! Change WALLET_PASSWORD to your own unique string.') + } + + console.log('📍 Address:', wallet.address) + console.log('🔗 View on explorer:', `https://amoy.polygonscan.com/address/${wallet.address}`) + + // Check balances + const polBalance = await provider.getBalance(wallet.address) + console.log('💰 POL Balance:', ethers.utils.formatEther(polBalance), 'POL') + + const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, wallet) + const usdcBalance = await usdc.balanceOf(wallet.address) + const decimals = await usdc.decimals() + console.log('💵 USDC Balance:', ethers.utils.formatUnits(usdcBalance, decimals), 'USDC') + + // Convert to 24-word mnemonic so you can import into any wallet + const mnemonic = ethers.Wallet.fromMnemonic( + ethers.utils.entropyToMnemonic(ethers.utils.hexZeroPad(wallet.privateKey, 32)), + ).mnemonic.phrase + console.log('\n📝 Mnemonic (24 words):', mnemonic) + console.log('💡 You can import this mnemonic into any wallet to check your balances:') + console.log(' • Nimiq Testnet Wallet: https://wallet.nimiq-testnet.com/ (supports Amoy USDC)') + console.log(' • Note: Nimiq Wallet does not support POL, only USDC/USDT') + console.log(' • Or use MetaMask, Trust Wallet, etc.\n') + + if (polBalance.eq(0)) { + console.log('\n⚠️ No POL found! Get free tokens from:') + console.log(' POL & USDC: https://faucet.polygon.technology/') + console.log(' USDC also: https://faucet.circle.com/') + console.log(' Then run this demo again!\n') + return + } + + // ========== LESSON 3: SENDING POL ========== + console.log('\n📚 LESSON 3: Sending POL Transactions') + console.log('─'.repeat(50)) + + const POL_AMOUNT = '0.0001' // Minimal amount for demo + + console.log('📤 Sending', POL_AMOUNT, 'POL to', RECIPIENT) + + try { + // Get current gas price and ensure minimum for Polygon + const feeData = await provider.getFeeData() + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas.lt(ethers.utils.parseUnits('30', 'gwei')) + ? ethers.utils.parseUnits('30', 'gwei') + : feeData.maxPriorityFeePerGas + + // Ensure maxFeePerGas is higher than maxPriorityFeePerGas + const maxFeePerGas = feeData.maxFeePerGas.lt(maxPriorityFeePerGas) + ? maxPriorityFeePerGas.mul(2) + : feeData.maxFeePerGas + + const tx = await wallet.sendTransaction({ + to: RECIPIENT, + value: ethers.utils.parseEther(POL_AMOUNT), + maxPriorityFeePerGas, + maxFeePerGas, + }) + console.log('⏳ Transaction hash:', tx.hash) + + const receipt = await tx.wait() + console.log('✅ Confirmed in block:', receipt.blockNumber) + console.log('⛽ Gas used:', receipt.gasUsed.toString()) + console.log('🔗 View:', `https://amoy.polygonscan.com/tx/${tx.hash}`) + + // Show balance change + const newPolBalance = await provider.getBalance(wallet.address) + const spent = polBalance.sub(newPolBalance) + console.log('📊 Total spent:', ethers.utils.formatEther(spent), 'POL (including gas)') + } + catch (error) { + console.log('❌ Failed:', error.message) + } + + // ========== LESSON 4: ERC20 & USDC ========== + console.log('\n📚 LESSON 4: ERC20 Tokens & USDC Transfers') + console.log('─'.repeat(50)) + + if (usdcBalance.eq(0)) { + console.log('⚠️ No USDC found! Get free USDC from:') + console.log(' https://faucet.polygon.technology/') + console.log(' https://faucet.circle.com/') + console.log(' Then try this section again!\n') + return + } + + const USDC_AMOUNT = '0.01' // Minimal amount for demo + console.log('📤 Sending', USDC_AMOUNT, 'USDC to', RECIPIENT) + + try { + // Get current gas price and ensure minimum for Polygon + const feeData = await provider.getFeeData() + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas.lt(ethers.utils.parseUnits('30', 'gwei')) + ? ethers.utils.parseUnits('30', 'gwei') + : feeData.maxPriorityFeePerGas + + // Ensure maxFeePerGas is higher than maxPriorityFeePerGas + const maxFeePerGas = feeData.maxFeePerGas.lt(maxPriorityFeePerGas) + ? maxPriorityFeePerGas.mul(2) + : feeData.maxFeePerGas + + const amountInBaseUnits = ethers.utils.parseUnits(USDC_AMOUNT, decimals) + const tx = await usdc.transfer(RECIPIENT, amountInBaseUnits, { + maxPriorityFeePerGas, + maxFeePerGas, + }) + console.log('⏳ Transaction hash:', tx.hash) + + const receipt = await tx.wait() + console.log('✅ Confirmed in block:', receipt.blockNumber) + console.log('🔗 View:', `https://amoy.polygonscan.com/tx/${tx.hash}`) + + // Show balance change + const newUsdcBalance = await usdc.balanceOf(wallet.address) + console.log('📊 New USDC balance:', ethers.utils.formatUnits(newUsdcBalance, decimals), 'USDC') + + const recipientBalance = await usdc.balanceOf(RECIPIENT) + console.log('📊 Recipient USDC:', ethers.utils.formatUnits(recipientBalance, decimals), 'USDC') + + // Show POL used for gas (still needed for ERC20!) + const finalPolBalance = await provider.getBalance(wallet.address) + console.log('⛽ POL balance:', ethers.utils.formatEther(finalPolBalance), 'POL (gas paid in POL!)') + } + catch (error) { + console.log('❌ Failed:', error.message) + } + + console.log('\n🎉 Demo complete! Now try lessons 2-4 step by step.') +} + +main().catch(console.error) diff --git a/src/content/tutorial/5-polygon-basics/1-introduction/_solution/lib/wallet.js b/src/content/tutorial/5-polygon-basics/1-introduction/_solution/lib/wallet.js new file mode 100644 index 0000000..5cc965c --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/1-introduction/_solution/lib/wallet.js @@ -0,0 +1,44 @@ +import { ethers } from 'ethers' + +/** + * Creates a wallet from a password string. + * + * WHY USE THIS? + * - You can recreate the same wallet anytime by using the same password + * - No need to save private keys in files or worry about losing them + * - Just remember your password and you can access your wallet from any code + * + * HOW IT WORKS: + * - Your password is hashed (keccak256) to create a deterministic private key + * - Same password = same private key = same wallet address every time + * + * SECURITY NOTE: + * - This is PERFECT for testnets (learning, experiments) + * - For mainnet with real money, use a hardware wallet or proper mnemonic phrase + */ +export function createWalletFromPassword(password) { + // Hash the password to get a deterministic private key + const privateKey = ethers.utils.id(password) + const wallet = new ethers.Wallet(privateKey) + + return wallet +} + +/** + * Alternative wallet creation methods: + * + * 1. FROM MNEMONIC (12/24 word phrase): + * const mnemonic = "word1 word2 word3 ... word12" + * const wallet = ethers.Wallet.fromMnemonic(mnemonic) + * + * 2. FROM PRIVATE KEY (hex string): + * const privateKey = "0x1234567890abcdef..." + * const wallet = new ethers.Wallet(privateKey) + * + * 3. RANDOM WALLET (new every time): + * const wallet = ethers.Wallet.createRandom() + * console.log('Save this mnemonic:', wallet.mnemonic.phrase) + * + * 4. FROM PASSWORD (this method - best for tutorials): + * const wallet = createWalletFromPassword("my_unique_password_123") + */ diff --git a/src/content/tutorial/5-polygon-basics/1-introduction/_solution/package.json b/src/content/tutorial/5-polygon-basics/1-introduction/_solution/package.json new file mode 100644 index 0000000..e13dae1 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/1-introduction/_solution/package.json @@ -0,0 +1 @@ +{ "name": "polygon-basics-demo", "type": "module", "version": "1.0.0", "scripts": { "demo": "node --watch index.js" }, "dependencies": { "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/5-polygon-basics/1-introduction/content.md b/src/content/tutorial/5-polygon-basics/1-introduction/content.md new file mode 100644 index 0000000..22a7482 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/1-introduction/content.md @@ -0,0 +1,107 @@ +--- +type: lesson +title: "Introduction to Polygon" +focus: /index.js +mainCommand: npm run demo +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Introduction to Polygon + +Welcome to Polygon Basics! Over the next three lessons you will assemble a complete transaction workflow on Polygon: create wallets, fund them with test assets, and move both native POL and ERC20 tokens. Each lesson flows step by step so you can focus on the concepts rather than deciphering shorthand. + +--- + +## Why Polygon? + +**Polygon** is an Ethereum Layer 2 network designed to feel familiar while solving Ethereum's biggest pain points. + +- **Same developer experience**: It speaks the Ethereum Virtual Machine (EVM), so tools like ethers.js, Hardhat, or MetaMask work without modification. +- **Lower transaction costs**: Fees are measured in fractions of a cent instead of whole dollars. +- **Faster confirmations**: Blocks land roughly every two seconds, keeping interactions snappy. +- **Ecosystem interoperability**: Assets and dApps can bridge between Polygon and Ethereum, so knowledge transfers directly. + +Think of Polygon as Ethereum's faster, more affordable sibling that still shares the family DNA. + +--- + +## Meet Polygon Amoy + +For this section we use **Polygon Amoy**, the current Polygon testnet. It mirrors mainnet behavior while using valueless tokens, which makes it ideal for experimentation. + +- **Network Name**: Polygon Amoy Testnet +- **Chain ID**: 80002 +- **RPC URL**: https://rpc-amoy.polygon.technology +- **Block Explorer**: https://amoy.polygonscan.com +- **Native Token**: POL (pays gas fees) + +Because every token on Amoy is free, you can try ideas, make mistakes, and rerun scripts without worrying about real money. + +--- + +## What You Will Build + +By the end of this part you will have a working toolkit for everyday Polygon development: + +### Lesson 2: Polygon Wallet Setup & Faucets + +- Generate an Ethereum-compatible wallet with ethers.js. +- Connect that wallet to Polygon Amoy. +- Collect free POL and USDC from public faucets. +- Read balances programmatically so you can verify funding. + +### Lesson 3: Sending POL Transactions + +- Craft and broadcast native POL transfers. +- Inspect gas usage and confirmation receipts. +- Follow the transaction lifecycle on PolygonScan. + +### Lesson 4: ERC20 Tokens & USDC Transfers + +- Review the ERC20 interface and why it matters. +- Interact with token contracts through ABIs. +- Send USDC and account for its six decimal places. + +Each lesson builds on the previous one, so keep your project files handy as you progress. + +--- + +## Why These Skills Matter + +Mastering Polygon translates directly to the broader EVM ecosystem: + +- Mainnet Ethereum and Layer 2 networks such as Optimism, Arbitrum, and Base share the same patterns. +- Sidechains like BNB Chain or Avalanche use identical wallet and contract workflows. +- Any project that relies on ethers.js or web3.js expects these fundamentals. + +Once you are comfortable on Polygon, you can approach most EVM-based platforms with confidence. + +--- + +## The Demo Script + +The code bundled with this lesson is a complete end-to-end walkthrough of everything you will build in Lessons 2-4. The demo runs automatically when you open this lesson—just check the terminal output. Treat it as a living reference: + +- **Review the terminal** to see the final experience in action. +- **Copy individual snippets** as you implement each step in the subsequent lessons. +- **Compare your work** against the finished version if you get stuck. + +The script demonstrates how to: + +1. Create a wallet and connect to Polygon Amoy. +2. Check POL and USDC balances. +3. Send POL to another address. +4. Transfer USDC (an ERC20 token) safely. + +> 💡 **Heads-up**: You will still need faucet funds before the demo shows non-zero balances. Lesson 2 covers that process. Until then you will see warnings about missing tokens. + +--- + +## Next Up + +Continue to **Lesson 2: Polygon Wallet Setup & Faucets** to create your first Polygon wallet and stock it with testnet tokens. diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/.env.example b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/.env.example new file mode 100644 index 0000000..c46dca9 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/.env.example @@ -0,0 +1,2 @@ +# Save your private key here after Step 6 +PRIVATE_KEY= \ No newline at end of file diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/index.js b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/index.js new file mode 100644 index 0000000..b3e9020 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/index.js @@ -0,0 +1,29 @@ +import { ethers } from 'ethers' +import { createWalletFromPassword } from './lib/wallet.js' + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const USDC_ADDRESS = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' +const USDC_ABI = ['function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)'] + +// 🔐 IMPORTANT: Use the SAME password you chose in lesson 1! +// Same password = same wallet address = your funds will be there +const WALLET_PASSWORD = 'change_me_to_something_unique_like_pizza_unicorn_2024' + +async function main() { + // TODO: Step 1 - Create wallet from your password (same as lesson 1) + // Hint: const wallet = createWalletFromPassword(WALLET_PASSWORD) + + // TODO: Step 2 - Connect wallet to Polygon Amoy provider + // Hint: const provider = new ethers.providers.JsonRpcProvider(RPC_URL) + // Hint: const connectedWallet = wallet.connect(provider) + + // TODO: Step 3 - Check POL balance + // Hint: const balance = await provider.getBalance(wallet.address) + // Hint: console.log('POL Balance:', ethers.utils.formatEther(balance)) + + // TODO: Step 4 - Check USDC balance + // Hint: const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, provider) + // Hint: const usdcBalance = await usdc.balanceOf(wallet.address) +} + +main().catch(console.error) diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/lib/wallet.js b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/lib/wallet.js new file mode 100644 index 0000000..5cc965c --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/lib/wallet.js @@ -0,0 +1,44 @@ +import { ethers } from 'ethers' + +/** + * Creates a wallet from a password string. + * + * WHY USE THIS? + * - You can recreate the same wallet anytime by using the same password + * - No need to save private keys in files or worry about losing them + * - Just remember your password and you can access your wallet from any code + * + * HOW IT WORKS: + * - Your password is hashed (keccak256) to create a deterministic private key + * - Same password = same private key = same wallet address every time + * + * SECURITY NOTE: + * - This is PERFECT for testnets (learning, experiments) + * - For mainnet with real money, use a hardware wallet or proper mnemonic phrase + */ +export function createWalletFromPassword(password) { + // Hash the password to get a deterministic private key + const privateKey = ethers.utils.id(password) + const wallet = new ethers.Wallet(privateKey) + + return wallet +} + +/** + * Alternative wallet creation methods: + * + * 1. FROM MNEMONIC (12/24 word phrase): + * const mnemonic = "word1 word2 word3 ... word12" + * const wallet = ethers.Wallet.fromMnemonic(mnemonic) + * + * 2. FROM PRIVATE KEY (hex string): + * const privateKey = "0x1234567890abcdef..." + * const wallet = new ethers.Wallet(privateKey) + * + * 3. RANDOM WALLET (new every time): + * const wallet = ethers.Wallet.createRandom() + * console.log('Save this mnemonic:', wallet.mnemonic.phrase) + * + * 4. FROM PASSWORD (this method - best for tutorials): + * const wallet = createWalletFromPassword("my_unique_password_123") + */ diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/package.json b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/package.json new file mode 100644 index 0000000..b5b73ff --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_files/package.json @@ -0,0 +1 @@ +{ "name": "polygon-wallet-setup", "type": "module", "version": "1.0.0", "scripts": { "dev": "node --watch index.js" }, "dependencies": { "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/.env.example b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/.env.example new file mode 100644 index 0000000..c46dca9 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/.env.example @@ -0,0 +1,2 @@ +# Save your private key here after Step 6 +PRIVATE_KEY= \ No newline at end of file diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/index.js b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/index.js new file mode 100644 index 0000000..0a29164 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/index.js @@ -0,0 +1,40 @@ +import { ethers } from 'ethers' +import { createWalletFromPassword } from './lib/wallet.js' + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const USDC_ADDRESS = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' +const USDC_ABI = ['function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)'] + +// 🔐 IMPORTANT: Use the SAME password you chose in lesson 1! +// Same password = same wallet address = your funds will be there +const WALLET_PASSWORD = 'change_me_to_something_unique_like_pizza_unicorn_2024' + +async function main() { + // Step 1: Create wallet from your password (same as lesson 1) + const wallet = createWalletFromPassword(WALLET_PASSWORD) + console.log('🔑 Your Wallet (from password)') + console.log('├─ Address:', wallet.address) + console.log('└─ Password:', WALLET_PASSWORD) + + if (WALLET_PASSWORD === 'change_me_to_something_unique_like_pizza_unicorn_2024') { + console.log('\n⚠️ Using default password! Change WALLET_PASSWORD to match lesson 1.') + } + + // Step 2: Connect to Polygon Amoy + const provider = new ethers.providers.JsonRpcProvider(RPC_URL) + const connectedWallet = wallet.connect(provider) + + // Step 3: Check POL balance + const balance = await provider.getBalance(wallet.address) + console.log('\\n💰 POL Balance:', ethers.utils.formatEther(balance), 'POL') + + // Step 5: Check USDC balance + const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, provider) + const usdcBalance = await usdc.balanceOf(wallet.address) + const decimals = await usdc.decimals() + console.log('💵 USDC Balance:', ethers.utils.formatUnits(usdcBalance, decimals), 'USDC') + + console.log('\\n💡 Save this private key to .env file for future lessons!') +} + +main().catch(console.error) diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/lib/wallet.js b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/lib/wallet.js new file mode 100644 index 0000000..5cc965c --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/lib/wallet.js @@ -0,0 +1,44 @@ +import { ethers } from 'ethers' + +/** + * Creates a wallet from a password string. + * + * WHY USE THIS? + * - You can recreate the same wallet anytime by using the same password + * - No need to save private keys in files or worry about losing them + * - Just remember your password and you can access your wallet from any code + * + * HOW IT WORKS: + * - Your password is hashed (keccak256) to create a deterministic private key + * - Same password = same private key = same wallet address every time + * + * SECURITY NOTE: + * - This is PERFECT for testnets (learning, experiments) + * - For mainnet with real money, use a hardware wallet or proper mnemonic phrase + */ +export function createWalletFromPassword(password) { + // Hash the password to get a deterministic private key + const privateKey = ethers.utils.id(password) + const wallet = new ethers.Wallet(privateKey) + + return wallet +} + +/** + * Alternative wallet creation methods: + * + * 1. FROM MNEMONIC (12/24 word phrase): + * const mnemonic = "word1 word2 word3 ... word12" + * const wallet = ethers.Wallet.fromMnemonic(mnemonic) + * + * 2. FROM PRIVATE KEY (hex string): + * const privateKey = "0x1234567890abcdef..." + * const wallet = new ethers.Wallet(privateKey) + * + * 3. RANDOM WALLET (new every time): + * const wallet = ethers.Wallet.createRandom() + * console.log('Save this mnemonic:', wallet.mnemonic.phrase) + * + * 4. FROM PASSWORD (this method - best for tutorials): + * const wallet = createWalletFromPassword("my_unique_password_123") + */ diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/package.json b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/package.json new file mode 100644 index 0000000..b5b73ff --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/_solution/package.json @@ -0,0 +1 @@ +{ "name": "polygon-wallet-setup", "type": "module", "version": "1.0.0", "scripts": { "dev": "node --watch index.js" }, "dependencies": { "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/content.md b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/content.md new file mode 100644 index 0000000..07f1c27 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/2-wallet-and-faucets/content.md @@ -0,0 +1,184 @@ +--- +type: lesson +title: "Polygon Wallet Setup & Faucets" +focus: /index.js +mainCommand: npm run dev +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Polygon Wallet Setup & Faucets + +Welcome to your first hands-on Polygon exercise. Before you can broadcast transactions or interact with smart contracts, you need two building blocks: a wallet that can sign messages and a source of test tokens. This lesson guides you through both with the same balance of narrative and structure used in the earlier chapters. + +--- + +## Learning Goals + +By the end of this lesson you will: + +- **Generate an Ethereum-compatible wallet** with ethers.js and understand the difference between its private key and public address. +- **Connect the wallet to Polygon Amoy**, Polygon's public testnet. +- **Collect free POL** to pay for gas in upcoming lessons. +- **Collect free USDC** so you can practice ERC20 transfers later on. +- **Store sensitive credentials safely** using environment variables. + +--- + +## Why It Matters + +Wallets sit at the heart of every Web3 interaction. Whether you are experimenting with DeFi, NFTs, or the gasless payments you will build in Section 6, you must be able to: + +- Create and back up keypairs responsibly. +- Talk to an RPC endpoint for the network you target. +- Verify balances before submitting transactions. +- Reuse credentials across scripts without exposing them. + +Master these fundamentals now and everything that follows will feel natural. + +--- + +## Polygon Amoy Recap + +We will work on **Polygon Amoy**, a no-stakes environment that mirrors Polygon mainnet: + +- **Network**: Polygon Amoy Testnet +- **Native Token**: POL (covers gas fees) +- **RPC URL**: https://rpc-amoy.polygon.technology +- **Chain ID**: 80002 + +Because tokens on Amoy have zero real-world value, you can experiment freely and rerun scripts as often as you like. + +--- + +## Step 1: Create a Wallet + +A wallet consists of a private key (keep it secret) and the public address you can share. Use ethers.js to generate both in one line: + +```js +import { ethers } from 'ethers' + +// Create a random wallet +const wallet = ethers.Wallet.createRandom() + +console.log('🔑 Your New Wallet') +console.log('├─ Address:', wallet.address) +console.log('└─ Private Key:', wallet.privateKey) +``` + +> ⚠️ **Security Note**: Logging private keys is acceptable in a controlled tutorial with test funds, but never do this in production or with real assets. + +--- + +## Step 2: Connect to Polygon Amoy + +Next, connect the wallet to an RPC endpoint so it can read state and submit transactions. + +```js +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const provider = new ethers.providers.JsonRpcProvider(RPC_URL) +const connectedWallet = wallet.connect(provider) +``` + +- `provider` handles network communication. +- `connectedWallet` binds your wallet to that provider, so signing and broadcasting are ready to go. + +--- + +## Step 3: Check Your Balance + +Fresh wallets start empty, but it is good practice to confirm that expectation programmatically. + +```js +const balance = await provider.getBalance(wallet.address) +console.log('\n💰 Balance:', ethers.utils.formatEther(balance), 'POL') +``` + +You should see `0.0 POL`, confirming the wallet has not been funded yet. + +--- + +## Step 4: Claim Free POL from the Faucet + +Faucets distribute play tokens for testnets. Follow these steps to fund your wallet with POL: + +1. Copy the address printed in your console. +2. Visit the Polygon faucet at **https://faucet.polygon.technology/**. +3. Choose "Polygon Amoy" from the dropdown. +4. Paste your address and submit the request. + +Within roughly 30 seconds the faucet should confirm the transfer. Run your script again to verify that the POL balance increased. + +--- + +## Step 5: Claim Free USDC + +Later lessons rely on an ERC20 token, so grab some USDC while you are here. You can get USDC from either of these faucets: + +**Option 1: Polygon Faucet** (also gives POL) + +1. Visit **https://faucet.polygon.technology/**. +2. Choose "Polygon Amoy" from the dropdown. +3. Paste your wallet address and submit. + +**Option 2: Circle Faucet** + +1. Open **https://faucet.circle.com/**. +2. Select "Polygon Amoy" as the network. +3. Paste your wallet address. +4. Complete the CAPTCHA and submit. + +To inspect your USDC balance you must query the token contract directly: + +```js +const USDC_ADDRESS = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' +const USDC_ABI = [ + 'function balanceOf(address) view returns (uint256)', + 'function decimals() view returns (uint8)' +] + +const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, provider) +const usdcBalance = await usdc.balanceOf(wallet.address) +const decimals = await usdc.decimals() + +console.log('\n💵 USDC Balance:', ethers.utils.formatUnits(usdcBalance, decimals), 'USDC') +``` + +--- + +## Step 6: Store the Private Key Securely + +Persist your wallet so future lessons can reuse it. Create a `.env` file and add your key: + +```bash +PRIVATE_KEY=your_private_key_here +``` + +Load it in your script before creating the wallet instance: + +```js +import dotenv from 'dotenv' + +dotenv.config() + +const wallet = new ethers.Wallet(process.env.PRIVATE_KEY) +``` + +> 💡 **Pro Tip**: Add `.env` to `.gitignore` so sensitive keys never end up in version control. + +--- + +## Wrap-Up + +You now have everything required for real Polygon workflows: + +- ✅ A reusable Ethereum-compatible wallet. +- ✅ POL to cover transaction fees on Polygon Amoy. +- ✅ USDC for ERC20 experiments. +- ✅ Environment variable management for safe credential storage. + +In the next lesson you will send your first on-chain POL transfer and watch it confirm in real time. diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/.env.example b/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/.env.example new file mode 100644 index 0000000..be028d2 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/.env.example @@ -0,0 +1,2 @@ +PRIVATE_KEY=your_private_key_from_lesson_1 +RECIPIENT=0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184 \ No newline at end of file diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/index.js b/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/index.js new file mode 100644 index 0000000..73fbab1 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/index.js @@ -0,0 +1,31 @@ +import { ethers } from 'ethers' +import { createWalletFromPassword } from './lib/wallet.js' + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const AMOUNT_POL = '0.0001' + +// 🔐 Use the SAME password from lesson 1 to access your wallet! +const WALLET_PASSWORD = 'change_me_to_something_unique_like_pizza_unicorn_2024' + +// Recipient address (Nimiq-controlled - see lesson 1 for details) +const RECIPIENT = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +async function main() { + // TODO: Step 1 - Load wallet from your password + // Hint: const wallet = createWalletFromPassword(WALLET_PASSWORD).connect(provider) + + // TODO: Step 2 - Check balance + // Hint: const balance = await provider.getBalance(wallet.address) + + // TODO: Step 3 - Prepare transaction (RECIPIENT already defined above) + + // TODO: Step 4 - Send transaction + // Hint: const tx = await wallet.sendTransaction({ to: RECIPIENT, value: ... }) + + // TODO: Step 5 - Wait for confirmation + // Hint: const receipt = await tx.wait() + + // TODO: Step 6 - Check updated balances +} + +main().catch(console.error) diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/lib/wallet.js b/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/lib/wallet.js new file mode 100644 index 0000000..5cc965c --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/lib/wallet.js @@ -0,0 +1,44 @@ +import { ethers } from 'ethers' + +/** + * Creates a wallet from a password string. + * + * WHY USE THIS? + * - You can recreate the same wallet anytime by using the same password + * - No need to save private keys in files or worry about losing them + * - Just remember your password and you can access your wallet from any code + * + * HOW IT WORKS: + * - Your password is hashed (keccak256) to create a deterministic private key + * - Same password = same private key = same wallet address every time + * + * SECURITY NOTE: + * - This is PERFECT for testnets (learning, experiments) + * - For mainnet with real money, use a hardware wallet or proper mnemonic phrase + */ +export function createWalletFromPassword(password) { + // Hash the password to get a deterministic private key + const privateKey = ethers.utils.id(password) + const wallet = new ethers.Wallet(privateKey) + + return wallet +} + +/** + * Alternative wallet creation methods: + * + * 1. FROM MNEMONIC (12/24 word phrase): + * const mnemonic = "word1 word2 word3 ... word12" + * const wallet = ethers.Wallet.fromMnemonic(mnemonic) + * + * 2. FROM PRIVATE KEY (hex string): + * const privateKey = "0x1234567890abcdef..." + * const wallet = new ethers.Wallet(privateKey) + * + * 3. RANDOM WALLET (new every time): + * const wallet = ethers.Wallet.createRandom() + * console.log('Save this mnemonic:', wallet.mnemonic.phrase) + * + * 4. FROM PASSWORD (this method - best for tutorials): + * const wallet = createWalletFromPassword("my_unique_password_123") + */ diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/package.json b/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/package.json new file mode 100644 index 0000000..ed011e2 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/_files/package.json @@ -0,0 +1 @@ +{ "name": "sending-pol", "type": "module", "version": "1.0.0", "scripts": { "send": "node --watch index.js" }, "dependencies": { "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/.env.example b/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/.env.example new file mode 100644 index 0000000..be028d2 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/.env.example @@ -0,0 +1,2 @@ +PRIVATE_KEY=your_private_key_from_lesson_1 +RECIPIENT=0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184 \ No newline at end of file diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/index.js b/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/index.js new file mode 100644 index 0000000..a417584 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/index.js @@ -0,0 +1,68 @@ +import { ethers } from 'ethers' +import { createWalletFromPassword } from './lib/wallet.js' + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const AMOUNT_POL = '0.0001' + +// 🔐 Use the SAME password from lesson 1 to access your wallet! +const WALLET_PASSWORD = 'change_me_to_something_unique_like_pizza_unicorn_2024' + +// Recipient address (Nimiq-controlled - see lesson 1 for details) +const RECIPIENT = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +async function main() { + // Step 1: Load wallet from your password + const provider = new ethers.providers.JsonRpcProvider(RPC_URL) + const wallet = createWalletFromPassword(WALLET_PASSWORD).connect(provider) + console.log('🔑 Sender Address:', wallet.address) + + // Step 2: Check balance + const balance = await provider.getBalance(wallet.address) + console.log('💰 Balance:', ethers.utils.formatEther(balance), 'POL') + + // Step 3: Set up transaction + console.log('\\n📤 Preparing Transaction') + console.log('├─ To:', RECIPIENT) + console.log('└─ Amount:', AMOUNT_POL, 'POL') + + // Step 4: Send transaction with proper gas settings + // Get current gas price and ensure minimum for Polygon + const feeData = await provider.getFeeData() + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas.lt(ethers.utils.parseUnits('30', 'gwei')) + ? ethers.utils.parseUnits('30', 'gwei') + : feeData.maxPriorityFeePerGas + + // Ensure maxFeePerGas is higher than maxPriorityFeePerGas + const maxFeePerGas = feeData.maxFeePerGas.lt(maxPriorityFeePerGas) + ? maxPriorityFeePerGas.mul(2) + : feeData.maxFeePerGas + + const tx = await wallet.sendTransaction({ + to: RECIPIENT, + value: ethers.utils.parseEther(AMOUNT_POL), + maxPriorityFeePerGas, + maxFeePerGas, + }) + console.log('\\n⏳ Transaction Sent!') + console.log('├─ Hash:', tx.hash) + console.log('├─ Explorer:', `https://amoy.polygonscan.com/tx/${tx.hash}`) + console.log('└─ Waiting for confirmation...') + + // Step 5: Wait for confirmation + const receipt = await tx.wait() + console.log('\\n✅ Transaction Confirmed!') + console.log('├─ Block:', receipt.blockNumber) + console.log('├─ Gas Used:', receipt.gasUsed.toString()) + console.log('├─ Status:', receipt.status === 1 ? 'Success' : 'Failed') + console.log('└─ View on Explorer:', `https://amoy.polygonscan.com/tx/${tx.hash}`) + + // Step 6: Check updated balances + const newBalance = await provider.getBalance(wallet.address) + const spent = balance.sub(newBalance) + console.log('\\n📊 Balance Update') + console.log('├─ Before:', ethers.utils.formatEther(balance), 'POL') + console.log('├─ After:', ethers.utils.formatEther(newBalance), 'POL') + console.log('└─ Spent:', ethers.utils.formatEther(spent), 'POL (including gas)') +} + +main().catch(console.error) diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/lib/wallet.js b/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/lib/wallet.js new file mode 100644 index 0000000..5cc965c --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/lib/wallet.js @@ -0,0 +1,44 @@ +import { ethers } from 'ethers' + +/** + * Creates a wallet from a password string. + * + * WHY USE THIS? + * - You can recreate the same wallet anytime by using the same password + * - No need to save private keys in files or worry about losing them + * - Just remember your password and you can access your wallet from any code + * + * HOW IT WORKS: + * - Your password is hashed (keccak256) to create a deterministic private key + * - Same password = same private key = same wallet address every time + * + * SECURITY NOTE: + * - This is PERFECT for testnets (learning, experiments) + * - For mainnet with real money, use a hardware wallet or proper mnemonic phrase + */ +export function createWalletFromPassword(password) { + // Hash the password to get a deterministic private key + const privateKey = ethers.utils.id(password) + const wallet = new ethers.Wallet(privateKey) + + return wallet +} + +/** + * Alternative wallet creation methods: + * + * 1. FROM MNEMONIC (12/24 word phrase): + * const mnemonic = "word1 word2 word3 ... word12" + * const wallet = ethers.Wallet.fromMnemonic(mnemonic) + * + * 2. FROM PRIVATE KEY (hex string): + * const privateKey = "0x1234567890abcdef..." + * const wallet = new ethers.Wallet(privateKey) + * + * 3. RANDOM WALLET (new every time): + * const wallet = ethers.Wallet.createRandom() + * console.log('Save this mnemonic:', wallet.mnemonic.phrase) + * + * 4. FROM PASSWORD (this method - best for tutorials): + * const wallet = createWalletFromPassword("my_unique_password_123") + */ diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/package.json b/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/package.json new file mode 100644 index 0000000..ed011e2 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/_solution/package.json @@ -0,0 +1 @@ +{ "name": "sending-pol", "type": "module", "version": "1.0.0", "scripts": { "send": "node --watch index.js" }, "dependencies": { "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/5-polygon-basics/3-sending-pol/content.md b/src/content/tutorial/5-polygon-basics/3-sending-pol/content.md new file mode 100644 index 0000000..ebeea53 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/3-sending-pol/content.md @@ -0,0 +1,173 @@ +--- +type: lesson +title: "Sending POL Transactions" +focus: /index.js +mainCommand: npm run send +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Sending POL Transactions + +With your wallet funded, it is time to perform a live transaction on Polygon Amoy. Understand the concept, walk through each step deliberately, and then experiment on your own. + +--- + +## Essential Context: POL + +**POL** (formerly called MATIC) is Polygon's native currency. Every on-chain action pays a small amount of POL as a gas fee, which compensates validators for processing your transaction. + +- **Token type**: Native protocol asset, similar to ETH on Ethereum. +- **Gas currency**: All Polygon fees are charged in POL. +- **Decimals**: 18 (same as ETH). + +Keep a small buffer of POL in your wallet; even ERC20 transfers consume it for gas. + +> 💡 **Nimiq contrast:** Nimiq blockchain has zero transaction fees. No gas token, no fee calculations—just send. We'll revisit this advantage in Section 6 when we tackle gasless transactions! + +--- + +## Step 1: Load the Wallet + +Reuse the `.env`-backed wallet you created earlier so you are not juggling multiple keys. + +```js +import dotenv from 'dotenv' +import { ethers } from 'ethers' + +dotenv.config() + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const provider = new ethers.providers.JsonRpcProvider(RPC_URL) +const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider) + +console.log('🔑 Sender Address:', wallet.address) +``` + +--- + +## Step 2: Confirm Available POL + +Always confirm you have enough POL to cover the transfer plus gas. + +```js +const balance = await provider.getBalance(wallet.address) +console.log('💰 Balance:', ethers.utils.formatEther(balance), 'POL') +``` + +Aim for at least `0.1 POL`. If you are low, revisit the faucet before continuing. + +--- + +## Step 3: Prepare the Transfer + +Choose a recipient address (another wallet you control works well) and decide how much to send. + +```js +const RECIPIENT = '0x...' // Replace with any address +const AMOUNT_POL = '0.0001' // Minimal amount for demo + +console.log('\n📤 Preparing Transaction') +console.log('├─ To:', RECIPIENT) +console.log('└─ Amount:', AMOUNT_POL, 'POL') +``` + +--- + +## Step 4: Broadcast the Transaction + +Let ethers.js handle signing and broadcasting with a single call. + +```js +const tx = await wallet.sendTransaction({ + to: RECIPIENT, + value: ethers.utils.parseEther(AMOUNT_POL) +}) + +console.log('\n⏳ Transaction Sent!') +console.log('├─ Hash:', tx.hash) +console.log('└─ Waiting for confirmation...') +``` + +- `sendTransaction` creates and submits the transaction. +- `parseEther` converts human-readable POL into wei, the smallest unit. +- The transaction hash is your receipt number; keep it handy. + +--- + +## Step 5: Wait for Confirmation + +Transactions settle once they are included in a block. Wait for that confirmation before moving on. + +```js +const receipt = await tx.wait() + +console.log('\n✅ Transaction Confirmed!') +console.log('├─ Block:', receipt.blockNumber) +console.log('├─ Gas Used:', receipt.gasUsed.toString()) +console.log('├─ Status:', receipt.status === 1 ? 'Success' : 'Failed') +console.log('└─ View on Explorer:', `https://amoy.polygonscan.com/tx/${tx.hash}`) +``` + +Open the explorer link to see the transaction exactly as validators processed it. + +--- + +## Step 6: Reconcile Balances + +Confirm both the transfer amount and the gas cost deducted from your wallet. + +```js +const newBalance = await provider.getBalance(wallet.address) +const spent = balance.sub(newBalance) + +console.log('\n📊 Balance Update') +console.log('├─ Before:', ethers.utils.formatEther(balance), 'POL') +console.log('├─ After:', ethers.utils.formatEther(newBalance), 'POL') +console.log('└─ Spent:', ethers.utils.formatEther(spent), 'POL (including gas)') +``` + +The difference between the sent amount and the total spent reflects the gas fee. + +--- + +## Understanding Gas Fees + +Every transfer has two cost components: + +1. **Amount transferred**: The value delivered to the recipient. +2. **Gas fee**: The computational cost paid to validators. + +``` +Gas Fee = Gas Used × Gas Price +``` + +- **Gas Used** represents the work done (a simple transfer is roughly 21,000 units). +- **Gas Price** fluctuates with network demand. + +On Polygon Amoy these fees are tiny, but cultivating the habit of checking them now will pay off on higher-cost networks. + +--- + +## Try-It Ideas + +- Send POL to yourself to see how the receipt looks when sender and recipient match. +- Experiment with smaller or larger amounts and observe how gas usage stays consistent. +- Submit multiple transactions in a row and compare their block numbers and confirmation times. + +--- + +## Wrap-Up + +You have now: + +- ✅ Broadcast your first Polygon transaction. +- ✅ Observed the full confirmation lifecycle. +- ✅ Calculated how gas fees affect balances. +- ✅ Gained confidence working with the POL native token. + +Next, you will apply the same discipline to ERC20 tokens by sending USDC. diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/.env.example b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/.env.example new file mode 100644 index 0000000..be028d2 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/.env.example @@ -0,0 +1,2 @@ +PRIVATE_KEY=your_private_key_from_lesson_1 +RECIPIENT=0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184 \ No newline at end of file diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/index.js b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/index.js new file mode 100644 index 0000000..815efa3 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/index.js @@ -0,0 +1,31 @@ +import { ethers } from 'ethers' +import { createWalletFromPassword } from './lib/wallet.js' + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const USDC_ADDRESS = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' +const AMOUNT = '0.01' + +// 🔐 Use the SAME password from lesson 1 to access your wallet! +const WALLET_PASSWORD = 'change_me_to_something_unique_like_pizza_unicorn_2024' + +// Recipient address (Nimiq-controlled - see lesson 1 for details) +const RECIPIENT = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +async function main() { + // TODO: Step 1 - Load wallet from your password + // Hint: const wallet = createWalletFromPassword(WALLET_PASSWORD).connect(provider) + + // TODO: Step 2 - Connect to USDC contract with ABI + // Hint: const USDC_ABI = ['function transfer(...)', 'function balanceOf(...)', ...] + // Hint: const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, wallet) + + // TODO: Step 3 - Check USDC balance + // Hint: const balance = await usdc.balanceOf(wallet.address) + + // TODO: Step 4 - Send USDC (RECIPIENT already defined above) + // Hint: const tx = await usdc.transfer(RECIPIENT, amountInBaseUnits) + + // TODO: Step 5 - Verify balances changed +} + +main().catch(console.error) diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/lib/wallet.js b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/lib/wallet.js new file mode 100644 index 0000000..5cc965c --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/lib/wallet.js @@ -0,0 +1,44 @@ +import { ethers } from 'ethers' + +/** + * Creates a wallet from a password string. + * + * WHY USE THIS? + * - You can recreate the same wallet anytime by using the same password + * - No need to save private keys in files or worry about losing them + * - Just remember your password and you can access your wallet from any code + * + * HOW IT WORKS: + * - Your password is hashed (keccak256) to create a deterministic private key + * - Same password = same private key = same wallet address every time + * + * SECURITY NOTE: + * - This is PERFECT for testnets (learning, experiments) + * - For mainnet with real money, use a hardware wallet or proper mnemonic phrase + */ +export function createWalletFromPassword(password) { + // Hash the password to get a deterministic private key + const privateKey = ethers.utils.id(password) + const wallet = new ethers.Wallet(privateKey) + + return wallet +} + +/** + * Alternative wallet creation methods: + * + * 1. FROM MNEMONIC (12/24 word phrase): + * const mnemonic = "word1 word2 word3 ... word12" + * const wallet = ethers.Wallet.fromMnemonic(mnemonic) + * + * 2. FROM PRIVATE KEY (hex string): + * const privateKey = "0x1234567890abcdef..." + * const wallet = new ethers.Wallet(privateKey) + * + * 3. RANDOM WALLET (new every time): + * const wallet = ethers.Wallet.createRandom() + * console.log('Save this mnemonic:', wallet.mnemonic.phrase) + * + * 4. FROM PASSWORD (this method - best for tutorials): + * const wallet = createWalletFromPassword("my_unique_password_123") + */ diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/package.json b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/package.json new file mode 100644 index 0000000..db2f2f6 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_files/package.json @@ -0,0 +1 @@ +{ "name": "erc20-usdc", "type": "module", "version": "1.0.0", "scripts": { "send": "node --watch index.js" }, "dependencies": { "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/.env.example b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/.env.example new file mode 100644 index 0000000..be028d2 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/.env.example @@ -0,0 +1,2 @@ +PRIVATE_KEY=your_private_key_from_lesson_1 +RECIPIENT=0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184 \ No newline at end of file diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/index.js b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/index.js new file mode 100644 index 0000000..fd1735e --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/index.js @@ -0,0 +1,69 @@ +import { ethers } from 'ethers' +import { createWalletFromPassword } from './lib/wallet.js' + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const USDC_ADDRESS = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' +const AMOUNT = '0.01' + +// 🔐 Use the SAME password from lesson 1 to access your wallet! +const WALLET_PASSWORD = 'change_me_to_something_unique_like_pizza_unicorn_2024' + +// Recipient address (Nimiq-controlled - see lesson 1 for details) +const RECIPIENT = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +async function main() { + // Step 1: Load wallet from your password + const provider = new ethers.providers.JsonRpcProvider(RPC_URL) + const wallet = createWalletFromPassword(WALLET_PASSWORD).connect(provider) + console.log('🔑 Sender:', wallet.address) + + // Step 2: Connect to USDC contract + const USDC_ABI = ['function transfer(address to, uint256 amount) returns (bool)', 'function balanceOf(address account) view returns (uint256)', 'function decimals() view returns (uint8)'] + const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, wallet) + + // Step 3: Check balance + const decimals = await usdc.decimals() + const balance = await usdc.balanceOf(wallet.address) + console.log('💵 USDC Balance:', ethers.utils.formatUnits(balance, decimals), 'USDC') + + // Step 4: Send USDC + console.log('\\n📤 Sending USDC') + console.log('├─ To:', RECIPIENT) + console.log('└─ Amount:', AMOUNT, 'USDC') + + // Get current gas price and ensure minimum for Polygon + const feeData = await provider.getFeeData() + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas.lt(ethers.utils.parseUnits('30', 'gwei')) + ? ethers.utils.parseUnits('30', 'gwei') + : feeData.maxPriorityFeePerGas + + // Ensure maxFeePerGas is higher than maxPriorityFeePerGas + const maxFeePerGas = feeData.maxFeePerGas.lt(maxPriorityFeePerGas) + ? maxPriorityFeePerGas.mul(2) + : feeData.maxFeePerGas + + const amountInBaseUnits = ethers.utils.parseUnits(AMOUNT, decimals) + const tx = await usdc.transfer(RECIPIENT, amountInBaseUnits, { + maxPriorityFeePerGas, + maxFeePerGas, + }) + console.log('\\n⏳ Transaction sent!') + console.log('├─ Hash:', tx.hash) + console.log('├─ Explorer:', `https://amoy.polygonscan.com/tx/${tx.hash}`) + console.log('└─ Waiting for confirmation...') + + const receipt = await tx.wait() + console.log('\\n✅ Confirmed in block:', receipt.blockNumber) + + // Step 5: Verify balances + const newBalance = await usdc.balanceOf(wallet.address) + const recipientBalance = await usdc.balanceOf(RECIPIENT) + console.log('\\n📊 Updated Balances') + console.log('├─ Your USDC:', ethers.utils.formatUnits(newBalance, decimals)) + console.log('└─ Recipient USDC:', ethers.utils.formatUnits(recipientBalance, decimals)) + + const polBalance = await provider.getBalance(wallet.address) + console.log('\\n⛽ Gas paid in POL:', ethers.utils.formatEther(polBalance)) +} + +main().catch(console.error) diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/lib/wallet.js b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/lib/wallet.js new file mode 100644 index 0000000..5cc965c --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/lib/wallet.js @@ -0,0 +1,44 @@ +import { ethers } from 'ethers' + +/** + * Creates a wallet from a password string. + * + * WHY USE THIS? + * - You can recreate the same wallet anytime by using the same password + * - No need to save private keys in files or worry about losing them + * - Just remember your password and you can access your wallet from any code + * + * HOW IT WORKS: + * - Your password is hashed (keccak256) to create a deterministic private key + * - Same password = same private key = same wallet address every time + * + * SECURITY NOTE: + * - This is PERFECT for testnets (learning, experiments) + * - For mainnet with real money, use a hardware wallet or proper mnemonic phrase + */ +export function createWalletFromPassword(password) { + // Hash the password to get a deterministic private key + const privateKey = ethers.utils.id(password) + const wallet = new ethers.Wallet(privateKey) + + return wallet +} + +/** + * Alternative wallet creation methods: + * + * 1. FROM MNEMONIC (12/24 word phrase): + * const mnemonic = "word1 word2 word3 ... word12" + * const wallet = ethers.Wallet.fromMnemonic(mnemonic) + * + * 2. FROM PRIVATE KEY (hex string): + * const privateKey = "0x1234567890abcdef..." + * const wallet = new ethers.Wallet(privateKey) + * + * 3. RANDOM WALLET (new every time): + * const wallet = ethers.Wallet.createRandom() + * console.log('Save this mnemonic:', wallet.mnemonic.phrase) + * + * 4. FROM PASSWORD (this method - best for tutorials): + * const wallet = createWalletFromPassword("my_unique_password_123") + */ diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/package.json b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/package.json new file mode 100644 index 0000000..db2f2f6 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/_solution/package.json @@ -0,0 +1 @@ +{ "name": "erc20-usdc", "type": "module", "version": "1.0.0", "scripts": { "send": "node --watch index.js" }, "dependencies": { "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/5-polygon-basics/4-erc20-usdc/content.md b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/content.md new file mode 100644 index 0000000..fa070e9 --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/4-erc20-usdc/content.md @@ -0,0 +1,204 @@ +--- +type: lesson +title: "ERC20 Tokens & USDC Transfers" +focus: /index.js +mainCommand: npm run send +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# ERC20 Tokens & USDC Transfers + +# ERC20 Tokens & USDC Transfers + +Native POL transfers are only half the story on Polygon. The majority of assets you will handle live inside smart contracts that follow the ERC20 standard. In this lesson you will apply everything you learned about wallets and providers to interact with **USDC**, a widely used stablecoin on Polygon Amoy. + +--- + +## ERC20 Primer + +**ERC20** defines a common interface that every compliant token must expose. Once you know the standard method names, you can work with almost any token: + +- `transfer(to, amount)` moves tokens between addresses. +- `balanceOf(address)` reports the balance for a specific holder. +- `approve(spender, amount)` grants another address permission to move tokens on your behalf. +- `decimals()` reveals how many decimal places the token uses. + +USDC conforms to this interface with the following Polygon Amoy details: + +- **Token address**: `0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582` +- **Decimals**: 6 (so 1 USDC equals 1,000,000 base units) + +--- + +## POL vs. USDC at a Glance + +| Feature | POL | USDC | +| --------------- | -------------------------- | ------------------------------------------ | +| Asset type | Native protocol token | ERC20 smart contract | +| Primary purpose | Pay gas fees | Represent and transfer dollar-pegged value | +| Transfer call | `wallet.sendTransaction()` | `contract.transfer()` | +| Decimal places | 18 | 6 | +| Who pays gas | Sender (in POL) | Still the sender (in POL) | + +Understanding this table clarifies why sending USDC feels slightly different even though the wallet and provider setup stays the same. + +--- + +## Step 1: Load the Wallet and Set Up the Contract + +```js +import dotenv from 'dotenv' +import { ethers } from 'ethers' + +dotenv.config() + +const RPC_URL = 'https://rpc-amoy.polygon.technology' +const USDC_ADDRESS = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' + +const provider = new ethers.providers.JsonRpcProvider(RPC_URL) +const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider) + +console.log('🔑 Sender:', wallet.address) +``` + +--- + +## Step 2: Describe the Contract with an ABI + +The Application Binary Interface (ABI) tells ethers.js which functions you plan to call. + +```js +const USDC_ABI = [ + 'function transfer(address to, uint256 amount) returns (bool)', + 'function balanceOf(address account) view returns (uint256)', + 'function decimals() view returns (uint8)' +] + +const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, wallet) +``` + +> 💡 We only include the fragments we need. PolygonScan hosts the full ABI if you ever require additional functions. + +--- + +## Step 3: Inspect Your USDC Balance + +Verify you have tokens to send before initiating a transfer. + +```js +const balance = await usdc.balanceOf(wallet.address) +const decimals = await usdc.decimals() + +console.log('💵 USDC Balance:', ethers.utils.formatUnits(balance, decimals), 'USDC') +``` + +`formatUnits` respects custom decimal counts; using `formatEther` here would incorrectly assume 18 decimals. + +--- + +## Step 4: Transfer USDC + +ERC20 transfers call the contract directly instead of using `sendTransaction`. + +```js +const RECIPIENT = process.env.RECIPIENT +const AMOUNT = '0.01' // Minimal amount for demo + +console.log('\n📤 Sending USDC') +console.log('├─ To:', RECIPIENT) +console.log('└─ Amount:', AMOUNT, 'USDC') + +const amountInBaseUnits = ethers.utils.parseUnits(AMOUNT, decimals) +const tx = await usdc.transfer(RECIPIENT, amountInBaseUnits) + +console.log('\n⏳ Transaction sent:', tx.hash) +const receipt = await tx.wait() + +console.log('✅ Confirmed in block:', receipt.blockNumber) +console.log('🔗 View:', `https://amoy.polygonscan.com/tx/${tx.hash}`) +``` + +Key differences from the POL workflow: + +- `usdc.transfer` submits a smart contract call. +- Gas is still charged in POL, not USDC. +- `parseUnits` uses the token's decimal count to avoid over- or under-paying. + +--- + +## Step 5: Reconcile Balances + +After confirmation, confirm that both your USDC and POL balances changed as expected. + +```js +const newBalance = await usdc.balanceOf(wallet.address) +const recipientBalance = await usdc.balanceOf(RECIPIENT) + +console.log('\n📊 Updated Balances') +console.log('├─ Your USDC:', ethers.utils.formatUnits(newBalance, decimals)) +console.log('└─ Recipient USDC:', ethers.utils.formatUnits(recipientBalance, decimals)) + +// Check POL was spent on gas +const polBalance = await provider.getBalance(wallet.address) +console.log('\n⛽ Gas paid in POL:', ethers.utils.formatEther(polBalance)) +``` + +You should see: + +- Your USDC balance drop by the transfer amount. +- Your POL balance dip slightly from gas costs. +- The recipient's USDC balance increases accordingly. + +--- + +## Gas Costs for ERC20 Transfers + +ERC20 transfers invoke smart contract logic, so they use more gas than native transfers: + +- Native POL transfer: about 21,000 gas. +- ERC20 transfer: typically 50,000 to 65,000 gas. + +Polygon's low fees mean the difference is small, but it is important to keep in mind on higher-cost networks. + +--- + +## Practice Suggestions + +- Try sending different amounts (for example 0.1 USDC or 10 USDC) and confirm the decimals stay accurate. +- Transfer tokens to your own address to see how the transaction appears in the logs. +- Execute several transfers and compare gas usage across each receipt. + +--- + +## Common Pitfalls + +❌ Using `parseEther` for USDC will multiply the amount by 10^12 and likely fail or drain your balance. + +```js +// WRONG (sends 1,000,000,000,000 USDC) +usdc.transfer(to, ethers.utils.parseEther('1')) + +// CORRECT (sends 1 USDC) +usdc.transfer(to, ethers.utils.parseUnits('1', 6)) +``` + +❌ Forgetting to keep POL on hand for gas will cause the transaction to revert. + +❌ Skipping the balance check may leave you guessing why a transfer failed. + +--- + +## Wrap-Up + +You now know how to: + +- ✅ Interact with ERC20 contracts through ethers.js. +- ✅ Send USDC on Polygon Amoy with precise decimal handling. +- ✅ Track both token balances and POL gas consumption. + +Next, in Part 6, you will learn how to cover those gas costs on behalf of your users with gasless transactions. diff --git a/src/content/tutorial/5-polygon-basics/meta.md b/src/content/tutorial/5-polygon-basics/meta.md new file mode 100644 index 0000000..192aaeb --- /dev/null +++ b/src/content/tutorial/5-polygon-basics/meta.md @@ -0,0 +1,12 @@ +--- +type: part +title: Polygon Basics +previews: false +prepareCommands: + - npm install +mainCommand: npm run dev +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- diff --git a/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_files/index.js b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_files/index.js new file mode 100644 index 0000000..3f50505 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_files/index.js @@ -0,0 +1,29 @@ +import { ethers } from 'ethers' + +async function main() { + console.log('🔐 Generating New Polygon Mainnet Wallet\n') + + // TODO: Create a random wallet using ethers.Wallet.createRandom() + + // TODO: Display the wallet address + + // TODO: Display the private key with a security warning + + // TODO: Display the 24-word mnemonic phrase + + console.log('\n⚠️ IMPORTANT: Save these credentials securely!') + console.log('• Never share your private key or mnemonic') + console.log('• Store them in a password manager or secure location') + console.log('• Anyone with these can access your funds') + + console.log('\n💡 TIP: Import your 24 words into Nimiq Wallet') + console.log(' Visit: https://wallet.nimiq.com') + console.log(' For a better user experience managing this wallet') + + console.log('\n💰 Get Mainnet Funds:') + console.log('• USDC: https://faucet.circle.com/') + console.log('• POL: https://faucet.polygon.technology/') + console.log('• USDT: No faucet available - purchase on exchange') +} + +main().catch(console.error) diff --git a/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_files/package.json b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_files/package.json new file mode 100644 index 0000000..c5e060b --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_files/package.json @@ -0,0 +1,5 @@ +{ + "type": "module", + "scripts": { "generate": "node --watch index.js" }, + "dependencies": { "ethers": "^5.7.2" } +} diff --git a/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_solution/index.js b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_solution/index.js new file mode 100644 index 0000000..ad88140 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_solution/index.js @@ -0,0 +1,35 @@ +import { ethers } from 'ethers' + +async function main() { + console.log('🔐 Generating New Polygon Mainnet Wallet\n') + + // Create a random wallet + const wallet = ethers.Wallet.createRandom() + + console.log('✅ Wallet Created!') + console.log('├─ Address:', wallet.address) + + // Display private key with warning + console.log('\n🔑 Private Key (keep this SECRET):') + console.log(' ', wallet.privateKey) + + // Display mnemonic phrase + console.log('\n📝 24-Word Mnemonic Phrase (keep this SECRET):') + console.log(' ', wallet.mnemonic.phrase) + + console.log('\n⚠️ IMPORTANT: Save these credentials securely!') + console.log('• Never share your private key or mnemonic') + console.log('• Store them in a password manager or secure location') + console.log('• Anyone with these can access your funds') + + console.log('\n💡 TIP: Import your 24 words into Nimiq Wallet') + console.log(' Visit: https://wallet.nimiq.com') + console.log(' For a better user experience managing this wallet') + + console.log('\n💰 Get Mainnet Funds:') + console.log('• USDC: https://faucet.circle.com/') + console.log('• POL: https://faucet.polygon.technology/') + console.log('• USDT: No faucet available - purchase on exchange') +} + +main().catch(console.error) diff --git a/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_solution/package.json b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_solution/package.json new file mode 100644 index 0000000..c5e060b --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/_solution/package.json @@ -0,0 +1,5 @@ +{ + "type": "module", + "scripts": { "generate": "node --watch index.js" }, + "dependencies": { "ethers": "^5.7.2" } +} diff --git a/src/content/tutorial/6-gasless-transfers/1-wallet-setup/content.md b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/content.md new file mode 100644 index 0000000..a41f763 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/1-wallet-setup/content.md @@ -0,0 +1,127 @@ +--- +type: lesson +title: "Mainnet Wallet Setup" +focus: /index.js +mainCommand: npm run generate +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Mainnet Wallet Setup + +Before working with gasless transactions on Polygon mainnet, you need a wallet with real funds. This lesson generates a fresh wallet and shows you how to secure it properly. + +--- + +## Why Mainnet? + +Unlike Section 5, which used the Amoy testnet, this section requires **Polygon mainnet**. OpenGSN relay networks operate on mainnet where real economic incentives keep relays running. The patterns you learn here translate directly to production apps. + +--- + +## Generate Your Wallet + +Run the script to create a random wallet. You will receive three pieces of information: + +1. **Address**: Your public identifier on Polygon. +2. **Private Key**: A 64-character hex string that controls your funds. +3. **24-Word Mnemonic**: A human-readable backup phrase. + +```js +import { ethers } from 'ethers' + +const wallet = ethers.Wallet.createRandom() + +console.log('Address:', wallet.address) +console.log('Private Key:', wallet.privateKey) +console.log('Mnemonic:', wallet.mnemonic.phrase) +``` + +--- + +## Security First + +⚠️ **CRITICAL**: Anyone with your private key or mnemonic can spend your funds. + +**Do:** + +- Store both in a password manager. +- Write the mnemonic on paper and keep it safe. +- Never share them with anyone. + +**Don't:** + +- Screenshot or email your credentials. +- Store them in plain text on your computer. +- Reuse this wallet for large amounts in production. + +> This is a learning wallet. Keep only enough funds to complete the tutorial (~5 USDT and 0.1 POL). + +--- + +## Import into Nimiq Wallet (Optional) + +For a better UX, import your 24-word mnemonic into the Nimiq Wallet: + +1. Visit **https://wallet.nimiq.com** +2. Select "Import with Recovery Words" +3. Paste your 24-word phrase +4. Access your wallet through a friendly interface + +The Nimiq Wallet supports Polygon and makes managing tokens easier than the command line. + +--- + +## Get Mainnet Funds + +You need two types of tokens: + +### USDC (for transfers) + +Visit **https://faucet.circle.com/** to get testnet USDC that works on mainnet. You will need 2-5 USDC to complete the gasless lessons. + +### POL (for gas in Lesson 2) + +Visit **https://faucet.polygon.technology/** to get a small amount of POL. You only need ~0.1 POL for the baseline gasful transaction in the next lesson. + +### USDT (no faucet available) + +There is no public faucet for USDT on Polygon mainnet. If you want to follow along with USDT examples instead of USDC, you will need to: + +- Purchase USDT on an exchange and withdraw to Polygon +- Swap USDC for USDT using a DEX like Uniswap +- Bridge USDT from Ethereum mainnet + +> For this tutorial, USDC is recommended since it has faucet access. + +--- + +## Save Your Private Key + +Copy your **private key** from the terminal output. You will paste it directly into the code files in the following lessons. Each lesson will have a placeholder like: + +```js +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' +``` + +Replace that placeholder with your actual private key. Never commit files with your real private key to version control. + +--- + +## Verify Your Setup + +Before moving forward, confirm: + +- ✅ Private key and mnemonic are stored securely +- ✅ Wallet address is funded with USDC and POL +- ✅ You understand the security risks + +--- + +## Next Up + +In **Lesson 2: Introduction to Gasless Transactions**, you will learn why gasless transactions matter and see the architecture that makes them possible. diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/_files/index.js b/src/content/tutorial/6-gasless-transfers/2-introduction/_files/index.js new file mode 100644 index 0000000..856e362 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/_files/index.js @@ -0,0 +1,120 @@ +import { ethers } from 'ethers' +import { checkBalances } from './lib/balances.js' +import { POLYGON_RPC_URL, TRANSFER_AMOUNT_USDT } from './lib/config.js' + +// 🔐 WALLET SETUP: Paste your private key from Lesson 1 here! +// ⚠️ This wallet needs USDT on Polygon MAINNET (not testnet) +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +console.log('🚀 Gasless Transactions - Complete Demo\n') +console.log('This demo shows the optimized gasless flow from Lesson 5:\n') +console.log('📚 What you\'ll see:') +console.log(' 1. Check USDT and POL balances') +console.log(' 2. Discover active relays from RelayHub') +console.log(' 3. Calculate optimal fees dynamically') +console.log(' 4. Send USDT without spending POL!') +console.log('\n⚠️ Note: This requires mainnet USDT and relay infrastructure') +console.log('─'.repeat(60)) + +// This is a simplified demo showing the concepts +// For the complete implementation, see lessons 2-5 + +async function main() { + const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) + + if (PRIVATE_KEY === '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1') { + console.log('\n⚠️ Using placeholder private key!') + console.log(' Run Lesson 1 to generate a wallet, then paste your private key above.\n') + return + } + + const wallet = new ethers.Wallet(PRIVATE_KEY, provider) + + console.log('\n💼 Wallet:', wallet.address) + + // Check balances + const balances = await checkBalances(provider, wallet) + console.log('💰 POL Balance:', balances.polFormatted, 'POL') + console.log('💵 USDT Balance:', balances.usdtFormatted, 'USDT') + + if (balances.usdt.eq(0)) { + console.log('\n❌ No USDT found! You need mainnet USDT to run this demo.') + console.log(' Get some from an exchange or bridge from Ethereum.\n') + return + } + + console.log('\n🔍 Step 1: Discovering Active Relays') + console.log('─'.repeat(60)) + console.log(' • Querying RelayHub for RelayServerRegistered events') + console.log(' • Looking back ~60 hours (144,000 blocks on Polygon)') + console.log(' • Validating relay health (version, balance, activity)') + console.log(' ⏳ This would take ~10-30 seconds in production...\n') + + // In production, you'd call discoverActiveRelay() here + // For demo purposes, we'll show what would happen + console.log(' ✅ Found relay: https://polygon-mainnet-relay.nimiq-network.com') + console.log(' ├─ Version: 2.2.6') + console.log(' ├─ Worker Balance: 0.15 POL') + console.log(' ├─ Base Fee: 0') + console.log(' ├─ PCT Fee: 15%') + console.log(' └─ Last Activity: 2 hours ago') + + console.log('\n💰 Step 2: Calculating Optimal Fee') + console.log('─'.repeat(60)) + + // Simplified fee calculation (see Lesson 5 for full version) + const networkGasPrice = await provider.getGasPrice() + console.log(' • Network gas price:', ethers.utils.formatUnits(networkGasPrice, 'gwei'), 'gwei') + + const bufferPercentage = 110 // 10% safety buffer + const bufferedGasPrice = networkGasPrice.mul(bufferPercentage).div(100) + console.log(' • Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'gwei'), 'gwei (10% buffer)') + + const gasLimit = 72000 // transferWithApproval gas limit + const baseCost = bufferedGasPrice.mul(gasLimit) + console.log(' • Base cost:', ethers.utils.formatEther(baseCost), 'POL') + + const pctRelayFee = 15 + const costWithPct = baseCost.mul(100 + pctRelayFee).div(100) + console.log(' • With relay fee:', ethers.utils.formatEther(costWithPct), 'POL (15% relay fee)') + + // Convert to USDT (simplified - in production use oracle) + const POL_PRICE = 0.50 // $0.50 per POL (example) + const feeInUSD = Number.parseFloat(ethers.utils.formatEther(costWithPct)) * POL_PRICE + const feeInUSDT = (feeInUSD * 1.10).toFixed(6) // 10% buffer + console.log(' • Fee in USDT:', feeInUSDT, 'USDT') + + console.log('\n📝 Step 3: Building Meta-Transaction') + console.log('─'.repeat(60)) + console.log(' • Signing USDT meta-approval (off-chain)') + console.log(' • Encoding transfer calldata') + console.log(' • Building relay request with fee') + console.log(' • Signing relay request with EIP-712') + console.log(' ✅ All signatures created (no gas spent!)') + + console.log('\n📡 Step 4: Submitting to Relay') + console.log('─'.repeat(60)) + console.log(' • Sending meta-transaction to relay server') + console.log(' • Relay validates and submits on-chain') + console.log(' • Relay pays gas in POL') + console.log(' • Contract reimburses relay in USDT') + + console.log('\n✅ Transaction Complete!') + console.log('─'.repeat(60)) + console.log(' 📊 Results:') + console.log(' ├─ USDT sent:', TRANSFER_AMOUNT_USDT, 'USDT') + console.log(' ├─ Relay fee paid:', feeInUSDT, 'USDT') + console.log(' ├─ POL spent: 0 POL (gasless!)') + console.log(' └─ Your POL balance: unchanged!') + + console.log('\n💡 To implement this for real:') + console.log(' 1. Complete Lesson 2 (gasful baseline)') + console.log(' 2. Complete Lesson 3 (static relay)') + console.log(' 3. Complete Lesson 4 (relay discovery)') + console.log(' 4. Complete Lesson 5 (optimized fees)') + console.log('\n🎉 Each lesson builds on the previous one!\n') +} + +main().catch((error) => { + console.error('\n❌ Error:', error.message) +}) diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/_files/lib/balances.js b/src/content/tutorial/6-gasless-transfers/2-introduction/_files/lib/balances.js new file mode 100644 index 0000000..ac5011b --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/_files/lib/balances.js @@ -0,0 +1,16 @@ +import { ethers } from 'ethers' +import { USDT_ABI, USDT_ADDRESS, USDT_DECIMALS } from './config.js' + +export async function checkBalances(provider, wallet) { + const polBalance = await provider.getBalance(wallet.address) + + const usdt = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider) + const usdtBalance = await usdt.balanceOf(wallet.address) + + return { + pol: polBalance, + usdt: usdtBalance, + polFormatted: ethers.utils.formatEther(polBalance), + usdtFormatted: ethers.utils.formatUnits(usdtBalance, USDT_DECIMALS), + } +} diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/_files/lib/config.js b/src/content/tutorial/6-gasless-transfers/2-introduction/_files/lib/config.js new file mode 100644 index 0000000..6105307 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/_files/lib/config.js @@ -0,0 +1,10 @@ +export const POLYGON_RPC_URL = 'https://polygon-rpc.com' + +export const USDT_ADDRESS = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F' +export const USDT_DECIMALS = 6 + +export const USDT_ABI = [ + 'function balanceOf(address) view returns (uint256)', +] + +export const TRANSFER_AMOUNT_USDT = '0.01' diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/_files/package.json b/src/content/tutorial/6-gasless-transfers/2-introduction/_files/package.json new file mode 100644 index 0000000..569b35a --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/_files/package.json @@ -0,0 +1 @@ +{ "name": "gasless-demo", "type": "module", "version": "1.0.0", "scripts": { "demo": "node --watch index.js" }, "dependencies": { "@opengsn/common": "^2.2.6", "axios": "^1.6.0", "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/index.js b/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/index.js new file mode 100644 index 0000000..856e362 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/index.js @@ -0,0 +1,120 @@ +import { ethers } from 'ethers' +import { checkBalances } from './lib/balances.js' +import { POLYGON_RPC_URL, TRANSFER_AMOUNT_USDT } from './lib/config.js' + +// 🔐 WALLET SETUP: Paste your private key from Lesson 1 here! +// ⚠️ This wallet needs USDT on Polygon MAINNET (not testnet) +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +console.log('🚀 Gasless Transactions - Complete Demo\n') +console.log('This demo shows the optimized gasless flow from Lesson 5:\n') +console.log('📚 What you\'ll see:') +console.log(' 1. Check USDT and POL balances') +console.log(' 2. Discover active relays from RelayHub') +console.log(' 3. Calculate optimal fees dynamically') +console.log(' 4. Send USDT without spending POL!') +console.log('\n⚠️ Note: This requires mainnet USDT and relay infrastructure') +console.log('─'.repeat(60)) + +// This is a simplified demo showing the concepts +// For the complete implementation, see lessons 2-5 + +async function main() { + const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) + + if (PRIVATE_KEY === '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1') { + console.log('\n⚠️ Using placeholder private key!') + console.log(' Run Lesson 1 to generate a wallet, then paste your private key above.\n') + return + } + + const wallet = new ethers.Wallet(PRIVATE_KEY, provider) + + console.log('\n💼 Wallet:', wallet.address) + + // Check balances + const balances = await checkBalances(provider, wallet) + console.log('💰 POL Balance:', balances.polFormatted, 'POL') + console.log('💵 USDT Balance:', balances.usdtFormatted, 'USDT') + + if (balances.usdt.eq(0)) { + console.log('\n❌ No USDT found! You need mainnet USDT to run this demo.') + console.log(' Get some from an exchange or bridge from Ethereum.\n') + return + } + + console.log('\n🔍 Step 1: Discovering Active Relays') + console.log('─'.repeat(60)) + console.log(' • Querying RelayHub for RelayServerRegistered events') + console.log(' • Looking back ~60 hours (144,000 blocks on Polygon)') + console.log(' • Validating relay health (version, balance, activity)') + console.log(' ⏳ This would take ~10-30 seconds in production...\n') + + // In production, you'd call discoverActiveRelay() here + // For demo purposes, we'll show what would happen + console.log(' ✅ Found relay: https://polygon-mainnet-relay.nimiq-network.com') + console.log(' ├─ Version: 2.2.6') + console.log(' ├─ Worker Balance: 0.15 POL') + console.log(' ├─ Base Fee: 0') + console.log(' ├─ PCT Fee: 15%') + console.log(' └─ Last Activity: 2 hours ago') + + console.log('\n💰 Step 2: Calculating Optimal Fee') + console.log('─'.repeat(60)) + + // Simplified fee calculation (see Lesson 5 for full version) + const networkGasPrice = await provider.getGasPrice() + console.log(' • Network gas price:', ethers.utils.formatUnits(networkGasPrice, 'gwei'), 'gwei') + + const bufferPercentage = 110 // 10% safety buffer + const bufferedGasPrice = networkGasPrice.mul(bufferPercentage).div(100) + console.log(' • Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'gwei'), 'gwei (10% buffer)') + + const gasLimit = 72000 // transferWithApproval gas limit + const baseCost = bufferedGasPrice.mul(gasLimit) + console.log(' • Base cost:', ethers.utils.formatEther(baseCost), 'POL') + + const pctRelayFee = 15 + const costWithPct = baseCost.mul(100 + pctRelayFee).div(100) + console.log(' • With relay fee:', ethers.utils.formatEther(costWithPct), 'POL (15% relay fee)') + + // Convert to USDT (simplified - in production use oracle) + const POL_PRICE = 0.50 // $0.50 per POL (example) + const feeInUSD = Number.parseFloat(ethers.utils.formatEther(costWithPct)) * POL_PRICE + const feeInUSDT = (feeInUSD * 1.10).toFixed(6) // 10% buffer + console.log(' • Fee in USDT:', feeInUSDT, 'USDT') + + console.log('\n📝 Step 3: Building Meta-Transaction') + console.log('─'.repeat(60)) + console.log(' • Signing USDT meta-approval (off-chain)') + console.log(' • Encoding transfer calldata') + console.log(' • Building relay request with fee') + console.log(' • Signing relay request with EIP-712') + console.log(' ✅ All signatures created (no gas spent!)') + + console.log('\n📡 Step 4: Submitting to Relay') + console.log('─'.repeat(60)) + console.log(' • Sending meta-transaction to relay server') + console.log(' • Relay validates and submits on-chain') + console.log(' • Relay pays gas in POL') + console.log(' • Contract reimburses relay in USDT') + + console.log('\n✅ Transaction Complete!') + console.log('─'.repeat(60)) + console.log(' 📊 Results:') + console.log(' ├─ USDT sent:', TRANSFER_AMOUNT_USDT, 'USDT') + console.log(' ├─ Relay fee paid:', feeInUSDT, 'USDT') + console.log(' ├─ POL spent: 0 POL (gasless!)') + console.log(' └─ Your POL balance: unchanged!') + + console.log('\n💡 To implement this for real:') + console.log(' 1. Complete Lesson 2 (gasful baseline)') + console.log(' 2. Complete Lesson 3 (static relay)') + console.log(' 3. Complete Lesson 4 (relay discovery)') + console.log(' 4. Complete Lesson 5 (optimized fees)') + console.log('\n🎉 Each lesson builds on the previous one!\n') +} + +main().catch((error) => { + console.error('\n❌ Error:', error.message) +}) diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/lib/balances.js b/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/lib/balances.js new file mode 100644 index 0000000..ac5011b --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/lib/balances.js @@ -0,0 +1,16 @@ +import { ethers } from 'ethers' +import { USDT_ABI, USDT_ADDRESS, USDT_DECIMALS } from './config.js' + +export async function checkBalances(provider, wallet) { + const polBalance = await provider.getBalance(wallet.address) + + const usdt = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider) + const usdtBalance = await usdt.balanceOf(wallet.address) + + return { + pol: polBalance, + usdt: usdtBalance, + polFormatted: ethers.utils.formatEther(polBalance), + usdtFormatted: ethers.utils.formatUnits(usdtBalance, USDT_DECIMALS), + } +} diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/lib/config.js b/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/lib/config.js new file mode 100644 index 0000000..6105307 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/lib/config.js @@ -0,0 +1,10 @@ +export const POLYGON_RPC_URL = 'https://polygon-rpc.com' + +export const USDT_ADDRESS = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F' +export const USDT_DECIMALS = 6 + +export const USDT_ABI = [ + 'function balanceOf(address) view returns (uint256)', +] + +export const TRANSFER_AMOUNT_USDT = '0.01' diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/package.json b/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/package.json new file mode 100644 index 0000000..569b35a --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/_solution/package.json @@ -0,0 +1 @@ +{ "name": "gasless-demo", "type": "module", "version": "1.0.0", "scripts": { "demo": "node --watch index.js" }, "dependencies": { "@opengsn/common": "^2.2.6", "axios": "^1.6.0", "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/6-gasless-transfers/2-introduction/content.md b/src/content/tutorial/6-gasless-transfers/2-introduction/content.md new file mode 100644 index 0000000..ea773b1 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/2-introduction/content.md @@ -0,0 +1,190 @@ +--- +type: lesson +title: "Introduction to Gasless Transactions" +focus: /index.js +mainCommand: npm run demo +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Introduction to Gasless Transactions + +Welcome to the last stretch of the tutorial series. By the end of this section you will know how to let users move stablecoins on Polygon **without holding any POL for gas**. If you can already send a regular ERC-20 transfer on Polygon, you have all the background you need—we will layer OpenGSN concepts on top in digestible steps. + +--- + +## Why Gas Gets in the Way + +Section 5 showed the standard Polygon flow: every transaction needs POL to pay gas. That single requirement creates friction at almost every touch point: + +- New users now juggle two tokens—POL for fees _and_ the asset they actually care about. +- Onboarding breaks whenever faucets run dry, exchanges delay KYC, or bridges are intimidating. +- Support teams keep answering the same question: _"Why can’t I just pay with the token I’m sending?"_ + +Our goal is to flip that experience so the end user only thinks about the asset they want to move. + +--- + +## Gasless Payments in Plain Language + +Think of OpenGSN as a courier service. Your user writes a letter (signs a request) and hands it to a courier (a relay). The courier pays the highway tolls (gas in POL) to deliver the letter on-chain. Once the job is done, your contract thanks the courier by reimbursing the tolls _plus_ a service fee in the token the user chose—USDT in our case. The user never had to touch POL. + +--- + +## OpenGSN Meta-Transactions Step by Step + +Let’s contrast the familiar “gasful” path with the OpenGSN equivalent: + +### Traditional (Gasful) Flow + +``` +1. User signs a transaction with their wallet. +2. The same wallet broadcasts the transaction and pays gas in POL. +3. Polygon executes the transaction. +``` + +### Gasless Flow with OpenGSN + +``` +1. User signs a meta-transaction off-chain. No gas is spent yet. +2. A relay server receives the signed payload and submits it on-chain, paying the POL gas up front. +3. Your smart contract (through a Paymaster) reimburses the relay in USDT and adds an agreed fee. +4. The user only spends USDT, even though the transaction settled on Polygon. +``` + +**Result:** The user can keep their POL balance at zero and still enjoy a successful transfer. + +--- + +## Key Players in the OpenGSN Stack + +- **Relay** – A server operated by the network or a provider that fronts the POL gas. You pay it back in tokens you control. +- **RelayHub** – The on-chain contract that coordinates relays, tracks their stakes, and holds deposits. +- **Forwarder** – Checks that the meta-transaction was genuinely signed by your user before calling your target contract. +- **Paymaster** – A contract you deploy that defines when and how a relay gets reimbursed. In this section, it pays the relay in USDT. +- **Meta-Transaction** – The signed payload that combines the “what to execute” instructions with relay payment terms and expirations. + +Keep these names in mind—the upcoming lessons will point back to them as you implement each part. + +--- + +## Roadmap for This Section + +Over four lessons we will evolve a gasless payment flow from “hello world” to production-ready: + +### Lesson 2: The Gasful Baseline + +- Send USDT the traditional way to measure the true gas cost. +- Record balances and receipts so you can compare later. + +### Lesson 3: Gasless with a Static Relay + +- Plug in a known relay URL and wire OpenGSN into your script. +- Sign EIP-712 payloads for approvals and relay requests. +- Complete a gasless USDT transfer where the relay fee is hardcoded. + +### Lesson 4: Discovering Relays Dynamically + +- Query RelayHub for active relays and verify their health signals. +- Fall back gracefully if a relay is offline or misconfigured. + +### Lesson 5: Optimized Fee Calculation + +- Derive dynamic fees from live gas prices and relay-specific terms. +- Apply safety buffers so you never underpay. +- Compare multiple relays and pick the cheapest healthy option—exactly what ships in the Nimiq wallet. + +--- + +## Why This Pattern Matters + +Gasless transactions unlock better UX across the board: + +- **Wallets:** Onboard users faster—no swapping or bridging needed before the first send. +- **Games:** Players stay immersed in in-game currencies instead of juggling gas tokens. +- **Payments:** Merchants collect USDT without explaining side costs to customers. +- **Onboarding:** First-time users succeed without leaving your app to acquire POL. + +You will see the same ideas in production systems such as the Nimiq Wallet, Biconomy, and Gelato. + +--- + +## Meta-Transactions Under the Hood + +Meta-transactions are simply “transactions about transactions.” Instead of sending the Polygon transaction yourself, you sign a message describing: + +- Which contract function to call (for example, `transfer` on USDT and the intended recipient). +- The gas budget and expiration rules the relay must respect. +- How much the relay should be paid back and in what token. + +When the relay submits that payload on-chain: + +1. The **Forwarder** verifies the signature really belongs to your user. +2. The Forwarder executes the requested contract call on your behalf. +3. Your contract (through the Paymaster) transfers the fee from your user to the relay. +4. The relay ends up whole—it recovers the POL it spent plus its service fee. + +### OpenGSN Architecture (High Level) + +``` +┌─────────┐ sign meta-tx ┌───────────┐ +│ User │ ─────────────────▶│ Relay │ +│ (no POL)│ │ Server │ +└─────────┘ └─────┬─────┘ + │ submits on-chain + │ (pays POL gas) + ▼ + ┌─────────────────┐ + │ RelayHub │ + │ (smart contract)│ + └────────┬────────┘ + │ validates + ▼ + ┌─────────────────┐ + │ Forwarder │ + │ (verifies sig) │ + └────────┬────────┘ + │ executes + ▼ + ┌─────────────────┐ + │ Your Contract │ + │ (transfer USDT) │ + │ (pay relay fee) │ + └─────────────────┘ +``` + +--- + +## Prerequisites + +⚠️ **Polygon mainnet is required.** OpenGSN is not deployed on the Amoy testnet. + +Make sure you have: + +- A wallet with **2-5 USDT** on Polygon mainnet (more is fine). +- A small buffer of **POL (0.01-0.1)** to cover the baseline transaction in Lesson 2. +- A **mainnet RPC endpoint** from Alchemy, Infura, or another provider. + +> 💡 Need USDT? Bridge it from Ethereum, swap on an exchange, or use any reputable source. You only need a couple of dollars for the exercises, but having a little extra makes debugging easier. + +--- + +## The Demo Script + +The accompanying script shows the **fully optimized flow from Lesson 5**. The demo runs automatically when you open this lesson—check the terminal output to watch the simulation unfold. Treat it as both a preview and a troubleshooting companion: + +- **Review the terminal** to see how relay discovery and fee calculation work together. +- **Revisit the code** as you implement each lesson to confirm your work. +- **Borrow patterns** for production projects once you understand every step. + +> 💡 This demo requires mainnet USDT in your wallet. Until you fund it you will see a warning about missing tokens, but the walkthrough still shows the sequence of calls so you can follow along conceptually. + +--- + +## Next Up + +Move on to **Lesson 2: The Gasful Baseline** to perform one last traditional transfer. You will measure the exact gas cost before we eliminate it with OpenGSN. diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/.env.example b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/.env.example new file mode 100644 index 0000000..9c23925 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/.env.example @@ -0,0 +1,4 @@ +POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/WuFIED8x3_klkj8fZ-zC8 +SENDER_PRIVATE_KEY=0x1b95cf9726a8ede24a8885609ccc83f5b7e30be5b441cad9e11e7715eff63175 +RECEIVER_ADDRESS=0x0000000000000000000000000000000000000000 +TRANSFER_AMOUNT_USDT=0.01 diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/index.js b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/index.js new file mode 100644 index 0000000..2fdad4c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/index.js @@ -0,0 +1,56 @@ +import { ethers } from 'ethers' +import { checkBalances } from './lib/balances.js' +import { POLYGON_CONFIG, USDT_ABI, USDT_DECIMALS } from './lib/constants.js' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// Recipient address (Nimiq-controlled - contact us if you need funds back) +const RECEIVER_ADDRESS = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +const TRANSFER_AMOUNT_USDT = '0.01' // Minimal amount for demo + +async function main() { + const POLYGON_RPC_URL = 'https://polygon-rpc.com' + + // STEP 1: create a provider connected to Polygon mainnet + const provider = undefined // TODO + + // STEP 2: load the funded wallet and connect it to the provider + const wallet = undefined // TODO + + console.log('Sender:', wallet.address) + console.log('Receiver:', RECEIVER_ADDRESS) + console.log('\n--- Balances before transfer ---') + + // STEP 3: Check balances using the helper function + const balancesBefore = undefined // TODO: call checkBalances(provider, wallet, RECEIVER_ADDRESS) + + console.log('Sender MATIC:', balancesBefore.sender.polFormatted, POLYGON_CONFIG.nativeSymbol) + console.log('Sender USDT:', balancesBefore.sender.usdtFormatted, 'USDT') + console.log('Receiver USDT:', balancesBefore.receiver.usdtFormatted, 'USDT') + + // STEP 4: parse the USDT amount and send the transfer transaction + const amountBaseUnits = undefined // TODO parseUnits + + console.log(`\nSending ${TRANSFER_AMOUNT_USDT} USDT to ${RECEIVER_ADDRESS}...`) + + // STEP 5: create USDT contract instance and transfer + const usdt = undefined // TODO: new ethers.Contract(POLYGON_CONFIG.usdtTokenAddress, USDT_ABI, wallet) + const transferTx = undefined // TODO call usdt.transfer + + console.log('Submitted tx:', `${POLYGON_CONFIG.explorerBaseUrl}${transferTx.hash}`) + const receipt = await transferTx.wait() + console.log('Mined in block', receipt.blockNumber) + + // STEP 6: re-check balances to confirm the transfer + const balancesAfter = undefined // TODO: call checkBalances(provider, wallet, RECEIVER_ADDRESS) + + console.log('\n--- Balances after transfer ---') + console.log('Sender USDT:', balancesAfter.sender.usdtFormatted, 'USDT') + console.log('Receiver USDT:', balancesAfter.receiver.usdtFormatted, 'USDT') +} + +main().catch((error) => { + console.error(error) +}) diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/lib/balances.js b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/lib/balances.js new file mode 100644 index 0000000..d7a7ca6 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/lib/balances.js @@ -0,0 +1,30 @@ +import { ethers } from 'ethers' +import { POLYGON_CONFIG, USDT_ABI, USDT_DECIMALS } from './constants.js' + +export async function checkBalances(provider, wallet, receiverAddress) { + const polBalance = await provider.getBalance(wallet.address) + + const usdt = new ethers.Contract( + POLYGON_CONFIG.usdtTokenAddress, + USDT_ABI, + wallet, + ) + + const [senderUsdt, receiverUsdt] = await Promise.all([ + usdt.balanceOf(wallet.address), + usdt.balanceOf(receiverAddress), + ]) + + return { + sender: { + pol: polBalance, + usdt: senderUsdt, + polFormatted: ethers.utils.formatEther(polBalance), + usdtFormatted: ethers.utils.formatUnits(senderUsdt, USDT_DECIMALS), + }, + receiver: { + usdt: receiverUsdt, + usdtFormatted: ethers.utils.formatUnits(receiverUsdt, USDT_DECIMALS), + }, + } +} diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/lib/constants.js b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/lib/constants.js new file mode 100644 index 0000000..00b398e --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/lib/constants.js @@ -0,0 +1,15 @@ +export const USDT_DECIMALS = 6 + +export const POLYGON_CONFIG = { + usdtTokenAddress: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', + explorerBaseUrl: 'https://polygonscan.com/tx/', + nativeSymbol: 'MATIC', +} + +export const USDT_ABI = [ + 'function name() view returns (string)', + 'function symbol() view returns (string)', + 'function decimals() view returns (uint8)', + 'function balanceOf(address owner) view returns (uint256)', + 'function transfer(address to, uint256 amount) returns (bool)', +] diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/package.json b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/package.json new file mode 100644 index 0000000..aa68f2c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_files/package.json @@ -0,0 +1,12 @@ +{ + "name": "polygon-usdc-mainnet-lab", + "type": "module", + "version": "0.1.0", + "private": true, + "scripts": { + "send": "node --watch index.js" + }, + "dependencies": { + "ethers": "^5.7.2" + } +} diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/.env.example b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/.env.example new file mode 100644 index 0000000..9c23925 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/.env.example @@ -0,0 +1,4 @@ +POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/WuFIED8x3_klkj8fZ-zC8 +SENDER_PRIVATE_KEY=0x1b95cf9726a8ede24a8885609ccc83f5b7e30be5b441cad9e11e7715eff63175 +RECEIVER_ADDRESS=0x0000000000000000000000000000000000000000 +TRANSFER_AMOUNT_USDT=0.01 diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/index.js b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/index.js new file mode 100644 index 0000000..2df9648 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/index.js @@ -0,0 +1,49 @@ +import { ethers } from 'ethers' +import { checkBalances } from './lib/balances.js' +import { POLYGON_CONFIG, USDT_ABI, USDT_DECIMALS } from './lib/constants.js' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// Recipient address (Nimiq-controlled - contact us if you need funds back) +const RECEIVER_ADDRESS = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +const TRANSFER_AMOUNT_USDT = '0.01' // Minimal amount for demo + +async function main() { + const POLYGON_RPC_URL = 'https://polygon-rpc.com' + + const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) + const wallet = new ethers.Wallet(PRIVATE_KEY, provider) + + console.log('Sender:', wallet.address) + console.log('Receiver:', RECEIVER_ADDRESS) + console.log('\n--- Balances before transfer ---') + + const balancesBefore = await checkBalances(provider, wallet, RECEIVER_ADDRESS) + + console.log('Sender MATIC:', balancesBefore.sender.polFormatted, POLYGON_CONFIG.nativeSymbol) + console.log('Sender USDT:', balancesBefore.sender.usdtFormatted, 'USDT') + console.log('Receiver USDT:', balancesBefore.receiver.usdtFormatted, 'USDT') + + const amountBaseUnits = ethers.utils.parseUnits(TRANSFER_AMOUNT_USDT, USDT_DECIMALS) + + console.log(`\nSending ${TRANSFER_AMOUNT_USDT} USDT to ${RECEIVER_ADDRESS}...`) + + const usdt = new ethers.Contract(POLYGON_CONFIG.usdtTokenAddress, USDT_ABI, wallet) + const transferTx = await usdt.transfer(RECEIVER_ADDRESS, amountBaseUnits) + + console.log('Submitted tx:', `${POLYGON_CONFIG.explorerBaseUrl}${transferTx.hash}`) + const receipt = await transferTx.wait() + console.log('Mined in block', receipt.blockNumber) + + const balancesAfter = await checkBalances(provider, wallet, RECEIVER_ADDRESS) + + console.log('\n--- Balances after transfer ---') + console.log('Sender USDT:', balancesAfter.sender.usdtFormatted, 'USDT') + console.log('Receiver USDT:', balancesAfter.receiver.usdtFormatted, 'USDT') +} + +main().catch((error) => { + console.error(error) +}) diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/lib/balances.js b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/lib/balances.js new file mode 100644 index 0000000..d7a7ca6 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/lib/balances.js @@ -0,0 +1,30 @@ +import { ethers } from 'ethers' +import { POLYGON_CONFIG, USDT_ABI, USDT_DECIMALS } from './constants.js' + +export async function checkBalances(provider, wallet, receiverAddress) { + const polBalance = await provider.getBalance(wallet.address) + + const usdt = new ethers.Contract( + POLYGON_CONFIG.usdtTokenAddress, + USDT_ABI, + wallet, + ) + + const [senderUsdt, receiverUsdt] = await Promise.all([ + usdt.balanceOf(wallet.address), + usdt.balanceOf(receiverAddress), + ]) + + return { + sender: { + pol: polBalance, + usdt: senderUsdt, + polFormatted: ethers.utils.formatEther(polBalance), + usdtFormatted: ethers.utils.formatUnits(senderUsdt, USDT_DECIMALS), + }, + receiver: { + usdt: receiverUsdt, + usdtFormatted: ethers.utils.formatUnits(receiverUsdt, USDT_DECIMALS), + }, + } +} diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/lib/constants.js b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/lib/constants.js new file mode 100644 index 0000000..00b398e --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/lib/constants.js @@ -0,0 +1,15 @@ +export const USDT_DECIMALS = 6 + +export const POLYGON_CONFIG = { + usdtTokenAddress: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', + explorerBaseUrl: 'https://polygonscan.com/tx/', + nativeSymbol: 'MATIC', +} + +export const USDT_ABI = [ + 'function name() view returns (string)', + 'function symbol() view returns (string)', + 'function decimals() view returns (uint8)', + 'function balanceOf(address owner) view returns (uint256)', + 'function transfer(address to, uint256 amount) returns (bool)', +] diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/package.json b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/package.json new file mode 100644 index 0000000..aa68f2c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/_solution/package.json @@ -0,0 +1,12 @@ +{ + "name": "polygon-usdc-mainnet-lab", + "type": "module", + "version": "0.1.0", + "private": true, + "scripts": { + "send": "node --watch index.js" + }, + "dependencies": { + "ethers": "^5.7.2" + } +} diff --git a/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/content.md b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/content.md new file mode 100644 index 0000000..4897f3b --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/3-gasful-baseline/content.md @@ -0,0 +1,76 @@ +--- +type: lesson +title: "Polygon USDT: The Gasful Baseline" +focus: /index.js +mainCommand: npm run send +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Polygon USDT: The Gasful Baseline + +Before we can appreciate gasless transfers, we need to measure the traditional cost. This lesson connects to Polygon mainnet, records your POL (formerly MATIC) and USDT balances, and sends a standard ERC20 transfer that consumes native gas. We will reuse the same credentials when we add OpenGSN in the next lesson, so keep them handy. + +--- + +## Step 1: Configure the Environment + +Copy `.env.example` to `.env`. The template includes a classroom key for demos, but you should replace or top it up with funds you control. Update the following variables: + +- `POLYGON_RPC_URL` - HTTPS endpoint for Polygon mainnet (Alchemy in the template). +- `SENDER_PRIVATE_KEY` - Mainnet wallet that holds both POL and USDT. +- `RECEIVER_ADDRESS` - Destination wallet you want to pay. + +> ⚠️ **Bring your own funds.** The shared key is public knowledge and might be empty when you attempt this tutorial. Make sure the sender wallet has a little POL for gas (0.01 is enough) and a few USDT before you proceed. + +--- + +## Step 2: Connect to Polygon + +Open `index.js` and wire up the provider: + +```js +const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) +``` + +Then load the funded wallet and attach it to the provider so future contract calls are signed automatically: + +```js +const wallet = new ethers.Wallet(SENDER_PRIVATE_KEY, provider) +``` + +Log both addresses to confirm the values pulled from `.env` are the ones you expect. + +--- + +## Step 3: Measure Balances Before Sending + +Check your native POL balance and both USDT balances (sender and receiver). The token address and ABI are already exported from `lib/constants.js`: + +```js +const usdt = new ethers.Contract(POLYGON_CONFIG.usdtTokenAddress, USDT_ABI, wallet) +const senderUsdtBefore = await usdt.balanceOf(wallet.address) +const receiverUsdtBefore = await usdt.balanceOf(RECEIVER_ADDRESS) +``` + +Use the helper `formatUsdt` to print readable values. This snapshot will highlight the gas spend and transfer amount later on. + +--- + +## Step 4: Send the Baseline Transfer + +Translate the human-readable amount from `.env` into base units, submit the transfer, and wait for the receipt: + +```js +const amountBaseUnits = ethers.utils.parseUnits(TRANSFER_AMOUNT_USDT, USDT_DECIMALS) +const transferTx = await usdt.transfer(RECEIVER_ADDRESS, amountBaseUnits) +const receipt = await transferTx.wait() +``` + +Print the PolygonScan link using `POLYGON_CONFIG.explorerBaseUrl`, then re-query the balances so the before-and-after values are obvious. + +Finally, run the script with `npm run send`. You should see the sender's POL balance drop slightly in addition to the USDT transfer. That gas cost is exactly what we will remove in the gasless version. diff --git a/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/.env.example b/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/.env.example new file mode 100644 index 0000000..46d13aa --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/.env.example @@ -0,0 +1,5 @@ +POLYGON_RPC_URL=https://polygon-rpc.com +SPONSOR_PRIVATE_KEY= +RECEIVER_ADDRESS=0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184 +TRANSFER_AMOUNT_USDT=0.01 +RELAY_URL=https://polygon-mainnet-relay.nimiq-network.com \ No newline at end of file diff --git a/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/index.js b/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/index.js new file mode 100644 index 0000000..d15d9f1 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/index.js @@ -0,0 +1,11 @@ +import { TypedRequestData } from '@opengsn/common/dist/EIP712/TypedRequestData.js' +import { getHttpClient } from '@opengsn/common/dist/HttpClient.js' +import { ethers } from 'ethers' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// TODO: Add the contract addresses +// TODO: Implement USDT approval signature +// TODO: Build and sign relay request +// TODO: Submit to static relay diff --git a/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/package.json b/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/package.json new file mode 100644 index 0000000..eb0292c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/4-static-relay/_files/package.json @@ -0,0 +1 @@ +{ "name": "gasless-static-relay", "type": "module", "version": "1.0.0", "scripts": { "gasless": "node --watch index.js" }, "dependencies": { "@opengsn/common": "^2.2.6", "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/6-gasless-transfers/4-static-relay/_solution/index.js b/src/content/tutorial/6-gasless-transfers/4-static-relay/_solution/index.js new file mode 100644 index 0000000..28ac42d --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/4-static-relay/_solution/index.js @@ -0,0 +1,197 @@ +import { HttpClient, HttpWrapper } from '@opengsn/common' +import { TypedRequestData } from '@opengsn/common/dist/EIP712/TypedRequestData.js' +import { ethers } from 'ethers' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// Mainnet addresses +const POLYGON_RPC_URL = 'https://polygon-rpc.com' +const USDT_ADDRESS = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F' +const TRANSFER_CONTRACT_ADDRESS = '0x98E69a6927747339d5E543586FC0262112eBe4BD' // USDT Transfer (Forwarder+Paymaster) +const RELAY_HUB_ADDRESS = '0x6C28AfC105e65782D9Ea6F2cA68df84C9e7d750d' // RelayHub v2.2.6 +const RECEIVER_ADDRESS = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' // Nimiq-controlled + +// Static relay (example - check if active) +const RELAY_URL = 'https://polygon-mainnet-relay.nimiq-network.com' + +const TRANSFER_AMOUNT_USDT = '0.01' // Minimal amount +const STATIC_FEE_USDT = '0.01' // Static relay fee + +// ABIs +const USDT_ABI = [ + 'function balanceOf(address) view returns (uint256)', + 'function transfer(address, uint256) returns (bool)', + 'function executeMetaTransaction(address from, bytes functionSignature, bytes32 sigR, bytes32 sigS, uint8 sigV) payable returns (bytes)', + 'function nonces(address) view returns (uint256)', +] + +const TRANSFER_ABI = [ + 'function transferWithApproval(address token, uint256 amount, address target, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)', + 'function getNonce(address) view returns (uint256)', +] + +async function main() { + console.log('🚀 Starting gasless USDT transfer with static relay...\n') + + const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) + const wallet = new ethers.Wallet(PRIVATE_KEY, provider) + + console.log('🔑 Sender:', wallet.address) + console.log('📍 Receiver:', RECEIVER_ADDRESS) + console.log('🔗 Relay:', RELAY_URL) + + // Step 1: Get USDT nonce + const usdt = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider) + const usdtNonce = await usdt.nonces(wallet.address) + + console.log('\n📝 USDT Nonce:', usdtNonce.toString()) + + // Step 2: Calculate approval amount + const transferAmount = ethers.utils.parseUnits(TRANSFER_AMOUNT_USDT, 6) + const feeAmount = ethers.utils.parseUnits(STATIC_FEE_USDT, 6) + const approvalAmount = transferAmount.add(feeAmount) + + console.log('💰 Transfer:', TRANSFER_AMOUNT_USDT, 'USDT') + console.log('💸 Fee:', STATIC_FEE_USDT, 'USDT') + console.log('✅ Total approval:', ethers.utils.formatUnits(approvalAmount, 6), 'USDT') + + // Step 3: Sign USDT approval (EIP-712 MetaTransaction) + const approveFunctionSignature = usdt.interface.encodeFunctionData('approve', [ + TRANSFER_CONTRACT_ADDRESS, + approvalAmount, + ]) + + const usdtDomain = { + name: 'USDT0', + version: '1', + verifyingContract: USDT_ADDRESS, + salt: ethers.utils.hexZeroPad(ethers.utils.hexlify(137), 32), // chainId as salt + } + + const usdtTypes = { + MetaTransaction: [ + { name: 'nonce', type: 'uint256' }, + { name: 'from', type: 'address' }, + { name: 'functionSignature', type: 'bytes' }, + ], + } + + const usdtMessage = { + nonce: usdtNonce.toNumber(), + from: wallet.address, + functionSignature: approveFunctionSignature, + } + + const approvalSignature = await wallet._signTypedData(usdtDomain, usdtTypes, usdtMessage) + const { r: sigR, s: sigS, v: sigV } = ethers.utils.splitSignature(approvalSignature) + + console.log('\n✍️ USDT approval signed') + + // Step 4: Build transfer calldata + const transferContract = new ethers.Contract(TRANSFER_CONTRACT_ADDRESS, TRANSFER_ABI, provider) + + const transferCalldata = transferContract.interface.encodeFunctionData('transferWithApproval', [ + USDT_ADDRESS, + transferAmount, + RECEIVER_ADDRESS, + feeAmount, + approvalAmount, + sigR, + sigS, + sigV, + ]) + + console.log('📦 Transfer calldata encoded') + + // Step 5: Get forwarder nonce + const forwarderNonce = await transferContract.getNonce(wallet.address) + const currentBlock = await provider.getBlockNumber() + const validUntil = currentBlock + (2 * 60 * 2) // 2 hours (2 blocks/min * 60 min * 2) + + console.log('🔢 Forwarder nonce:', forwarderNonce.toString()) + + // Step 6: Build relay request + const relayRequest = { + request: { + from: wallet.address, + to: TRANSFER_CONTRACT_ADDRESS, + value: '0', + gas: '350000', // Static gas limit + nonce: forwarderNonce.toString(), + data: transferCalldata, + validUntil: validUntil.toString(), + }, + relayData: { + gasPrice: '100000000000', // 100 gwei - static! + pctRelayFee: '0', + baseRelayFee: '0', + relayWorker: '0x0000000000000000000000000000000000000000', // Will be filled by relay + paymaster: TRANSFER_CONTRACT_ADDRESS, + forwarder: TRANSFER_CONTRACT_ADDRESS, + paymasterData: '0x', + clientId: '1', + }, + } + + // Step 7: Sign relay request + const forwarderDomain = { + name: 'Forwarder', + version: '1', + chainId: 137, + verifyingContract: TRANSFER_CONTRACT_ADDRESS, + } + + const { types, domain, primaryType, message } = new TypedRequestData( + forwarderDomain.chainId.toString(), + forwarderDomain.verifyingContract, + relayRequest, + ) + + const relaySignature = await wallet._signTypedData(domain, types, message) + + console.log('✍️ Relay request signed') + + // Step 8: Get relay worker address + console.log('\n🔍 Pinging relay...') + const relayInfo = await fetch(`${RELAY_URL}/getaddr`).then(r => r.json()) + + if (!relayInfo.ready) { + throw new Error('Relay is not ready') + } + + console.log('✅ Relay ready, worker:', relayInfo.relayWorkerAddress) + + // Update relayWorker in the request + relayRequest.relayData.relayWorker = relayInfo.relayWorkerAddress + + // Step 9: Get current relay nonce + const relayNonce = await provider.getTransactionCount(relayInfo.relayWorkerAddress) + + // Step 10: Submit to relay + console.log('\n📡 Submitting to relay...') + + const httpClient = new HttpClient(new HttpWrapper(), console) + const relayResponse = await httpClient.relayTransaction(RELAY_URL, { + relayRequest, + metadata: { + signature: relaySignature, + approvalData: '0x', + relayHubAddress: RELAY_HUB_ADDRESS, + relayMaxNonce: relayNonce + 3, + }, + }) + + const txHash = typeof relayResponse === 'string' + ? relayResponse + : relayResponse.signedTx || relayResponse.txHash + + console.log('\n✅ Gasless transaction sent!') + console.log('🔗 View:', `https://polygonscan.com/tx/${txHash}`) + console.log('\n💡 Your wallet POL balance was NOT spent!') + console.log(' The relay paid the gas and was reimbursed in USDT.') +} + +main().catch((error) => { + console.error('\n❌ Error:', error.message) +}) diff --git a/src/content/tutorial/6-gasless-transfers/4-static-relay/_solution/package.json b/src/content/tutorial/6-gasless-transfers/4-static-relay/_solution/package.json new file mode 100644 index 0000000..eb0292c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/4-static-relay/_solution/package.json @@ -0,0 +1 @@ +{ "name": "gasless-static-relay", "type": "module", "version": "1.0.0", "scripts": { "gasless": "node --watch index.js" }, "dependencies": { "@opengsn/common": "^2.2.6", "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/6-gasless-transfers/4-static-relay/content.md b/src/content/tutorial/6-gasless-transfers/4-static-relay/content.md new file mode 100644 index 0000000..b42a419 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/4-static-relay/content.md @@ -0,0 +1,282 @@ +--- +type: lesson +title: "Gasless with Static Relay" +focus: /index.js +mainCommand: npm run gasless +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Gasless with Static Relay + +You just measured the cost of a standard USDT transfer. Now we will send the same payment through **OpenGSN**, where a relay covers the POL gas and you reimburse it in USDT. This first iteration keeps everything intentionally simple so you can see each moving part clearly before layering on optimizations in later lessons. + +--- + +## OpenGSN at a Glance + +The gasless flow has three stages: + +1. You sign a meta-transaction off-chain. No POL is spent yet. +2. A relay server broadcasts that request on-chain and pays the POL gas. +3. Your transfer contract reimburses the relay in USDT (amount plus fee). + +Key roles involved: + +- **Sponsor (you)** signs messages and funds relay fees in USDT. +- **Relay servers** front the POL gas and expect reimbursement. +- **Transfer contract** executes the token move and fee payment. +- **RelayHub** validates and routes meta-transactions across the network. + +If you have never met OpenGSN before, keep this component cheat sheet handy: + +- **Forwarder:** verifies the meta-transaction signature and keeps per-sender nonces so relays cannot replay old requests. The Nimiq transfer contract bundles a forwarder implementation; see the reference in the [Nimiq Developer Center](https://developers.nimiq.com/). +- **Paymaster:** refunds the relay in tokens such as USDT or USDC. For this tutorial the same transfer contract doubles as paymaster. +- **RelayHub:** the canonical on-chain registry of relays. Its API is documented in the [OpenGSN Docs](https://docs.opengsn.org/). +- **Relay server:** an off-chain service that watches the hub and exposes `/getaddr` plus `/relay` endpoints. Polygon’s networking requirements for relays are outlined in the [Polygon developer documentation](https://docs.polygon.technology/). + +--- + +## Guardrails for This Lesson + +To keep the walkthrough approachable we will: + +- Hardcode a known relay URL instead of discovering one dynamically. +- Use a static relay fee (0.1 USDT) and a fixed gas price. +- Work entirely on Polygon mainnet because OpenGSN is not deployed on Amoy. + +Later lessons will replace each shortcut with production logic. + +--- + +## Step 1: Configure Environment Variables + +Create or update your `.env` file with the following values: + +```bash title=".env" +POLYGON_RPC_URL=https://polygon-rpc.com +SPONSOR_PRIVATE_KEY=your_mainnet_key_with_USDT +RECEIVER_ADDRESS=0x... +TRANSFER_AMOUNT_USDT=1.0 +RELAY_URL=https://polygon-mainnet-relay.nimiq-network.com +``` + +> ⚠️ **Mainnet required:** you need a mainnet wallet that holds at least 1-2 USDT and a small amount of POL. Acquire funds via your preferred exchange or bridge service. + +--- + +## Step 2: Connect and Define Contract Addresses + +```js title="index.js" showLineNumbers mark=6-13 +import dotenv from 'dotenv' +import { ethers } from 'ethers' + +dotenv.config() + +const provider = new ethers.providers.JsonRpcProvider(process.env.POLYGON_RPC_URL) +const wallet = new ethers.Wallet(process.env.SPONSOR_PRIVATE_KEY, provider) + +// Contract addresses (Polygon mainnet) +const USDT_ADDRESS = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F' +const TRANSFER_CONTRACT_ADDRESS = '0x...' // Nimiq's transfer contract +const RELAY_HUB_ADDRESS = '0x...' // OpenGSN RelayHub + +console.log('🔑 Sponsor:', wallet.address) +``` + +The sponsor wallet is the account that will sign messages and reimburse the relay. +The concrete contract addresses are published in `@cashlink/currency`’s constants module and mirrored in the [Nimiq wallet gasless guide](https://developers.nimiq.com/). Always verify them against the latest deployment notes before running on mainnet. + +--- + +## Step 3: Retrieve the USDT Nonce and Approval Amount + +USDT on Polygon does _not_ implement the standard ERC‑2612 permit. Instead it exposes `executeMetaTransaction`, which expects you to sign the encoded `approve` call. The `nonces` counter you query below is USDT’s own meta-transaction nonce (documented in [Tether’s contract implementation](https://docs.opengsn.org/contracts/erc-2771.html)), so we can safely reuse it when we sign the approval. + +Fetch the current nonce and compute how much the transfer contract is allowed to spend (transfer amount + relay fee). + +```js title="index.js" showLineNumbers mark=3-9 +const USDT_ABI = ['function nonces(address owner) view returns (uint256)'] +const usdt = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider) + +const nonce = await usdt.nonces(wallet.address) +console.log('📝 USDT Nonce:', nonce.toString()) + +// Calculate amounts +const amountToSend = ethers.utils.parseUnits(process.env.TRANSFER_AMOUNT_USDT, 6) +const staticFee = ethers.utils.parseUnits('0.1', 6) // 0.1 USDT fee (static!) +const approvalAmount = amountToSend.add(staticFee) +``` + +--- + +## Step 4: Sign the USDT Meta-Approval + +USDT on Polygon uses `executeMetaTransaction` for gasless approvals. Build the EIP‑712 MetaTransaction payload and sign it. Notice the domain uses the `salt` field instead of `chainId`; that is specific to the USDT contract. Compare this to the generic permit flow covered in [OpenGSN’s meta-transaction docs](https://docs.opengsn.org/gsn-provider/metatx.html) to see the differences. + +```js title="index.js" showLineNumbers mark=9-19 +// First, encode the approve function call +const approveFunctionSignature = usdt.interface.encodeFunctionData('approve', [ + TRANSFER_CONTRACT_ADDRESS, + approvalAmount +]) + +// Build the MetaTransaction EIP-712 domain +const domain = { + name: 'USDT0', + version: '1', + verifyingContract: USDT_ADDRESS, + salt: ethers.utils.hexZeroPad(ethers.utils.hexlify(137), 32) // chainId as salt +} + +const types = { + MetaTransaction: [ + { name: 'nonce', type: 'uint256' }, + { name: 'from', type: 'address' }, + { name: 'functionSignature', type: 'bytes' } + ] +} + +const message = { + nonce: nonce.toNumber(), + from: wallet.address, + functionSignature: approveFunctionSignature +} + +const signature = await wallet._signTypedData(domain, types, message) +const { r, s, v } = ethers.utils.splitSignature(signature) + +console.log('✍️ USDT approval signed') +``` + +This signature allows the relay to execute the `approve` call on your behalf via `executeMetaTransaction`. + +--- + +## Step 5: Encode the Transfer Call + +Prepare the calldata the relay will submit on your behalf. + +```js title="index.js" showLineNumbers mark=6-14 +const TRANSFER_ABI = ['function transferWithApproval(address token, uint256 amount, address to, uint256 fee, uint256 approval, bytes32 r, bytes32 s, uint8 v)'] +const transferContract = new ethers.Contract(TRANSFER_CONTRACT_ADDRESS, TRANSFER_ABI, wallet) + +const transferCalldata = transferContract.interface.encodeFunctionData('transferWithApproval', [ + USDT_ADDRESS, + amountToSend, + process.env.RECEIVER_ADDRESS, + staticFee, + approvalAmount, + r, + s, + v +]) + +console.log('📦 Calldata encoded') +``` + +--- + +## Step 6: Build and Sign the Relay Request + +The relay expects a second EIP‑712 signature covering the meta-transaction wrapper. This time the domain is the **forwarder** (embedded inside the transfer contract). Gather the contract nonce and sign the payload. + +```js title="index.js" showLineNumbers mark=6-21 +const transferNonce = await transferContract.getNonce(wallet.address) + +const relayRequest = { + request: { + from: wallet.address, + to: TRANSFER_CONTRACT_ADDRESS, + value: '0', + gas: '350000', + nonce: transferNonce.toString(), + data: transferCalldata, + validUntil: (Math.floor(Date.now() / 1000) + 7200).toString() + }, + relayData: { + gasPrice: '100000000000', // 100 gwei (static!) + pctRelayFee: '0', + baseRelayFee: '0', + relayWorker: '0x0000000000000000000000000000000000000000', // Will be filled by relay + paymaster: TRANSFER_CONTRACT_ADDRESS, + forwarder: TRANSFER_CONTRACT_ADDRESS, + paymasterData: '0x', + clientId: '1' + } +} + +// Sign it +const relayDomain = { name: 'GSN Relayed Transaction', version: '2', chainId: 137, verifyingContract: TRANSFER_CONTRACT_ADDRESS } +const relayTypes = { /* RelayRequest types – see docs.opengsn.org for the full schema */ } +const relaySignature = await wallet._signTypedData(relayDomain, relayTypes, relayRequest) + +console.log('✍️ Relay request signed') +``` + +--- + +## Step 7: Submit the Meta-Transaction + +Use the OpenGSN HTTP client to send the request to your chosen relay. The worker nonce check prevents you from handing the relay a `relayMaxNonce` that is already stale—if the worker broadcasts several transactions in quick succession, your request will still slide in. Likewise, `validUntil` in the previous step protects the relay from signing requests that could be replayed months later. + +```js title="index.js" showLineNumbers mark=1-18 +import { HttpClient, HttpWrapper } from '@opengsn/common' + +const relayNonce = await provider.getTransactionCount(relayInfo.relayWorkerAddress) + +const httpClient = new HttpClient(new HttpWrapper(), console) +const relayResponse = await httpClient.relayTransaction(RELAY_URL, { + relayRequest, + metadata: { + signature: relaySignature, + approvalData: '0x', + relayHubAddress: RELAY_HUB_ADDRESS, + relayMaxNonce: relayNonce + 3 + } +}) + +const txHash = typeof relayResponse === 'string' + ? relayResponse + : relayResponse.signedTx || relayResponse.txHash + +console.log('\n✅ Gasless transaction sent!') +console.log('🔗 View:', `https://polygonscan.com/tx/${txHash}`) +``` + +--- + +## Recap: What Just Happened + +1. You signed a USDT meta-approval without spending gas. +2. You signed a meta-transaction request for the relay. +3. The relay paid POL to submit the transaction on-chain. +4. The receiver received USDT minus the 0.1 USDT relay fee. +5. Your wallet retained its POL balance. + +--- + +## Limitations to Keep in Mind + +- ❌ Hardcoded relay URL (no fallback if it goes offline). +- ❌ Static fee and gas price (no adaptation to network conditions). +- ❌ No validation of relay health beyond a single request. + +The next lessons address each of these gaps. + +--- + +## Wrap-Up + +You have now: + +- ✅ Sent USDT without paying POL yourself. +- ✅ Practiced constructing and signing OpenGSN meta-transactions. +- ✅ Understood the flow between approval, relay request, and paymaster contract. +- ✅ Prepared the foundation for relay discovery and fee optimization. + +Next up, **Lesson 5** walks through discovering relays dynamically from the RelayHub and filtering them with health checks informed by the [OpenGSN relay operator guide](https://docs.opengsn.org/relay/). That will let you replace today’s hardcoded URL with resilient discovery logic. diff --git a/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/.env.example b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/.env.example new file mode 100644 index 0000000..a1ca84c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/.env.example @@ -0,0 +1,4 @@ +POLYGON_RPC_URL=https://polygon-rpc.com +SPONSOR_PRIVATE_KEY= +RECEIVER_ADDRESS=0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184 +TRANSFER_AMOUNT_USDT=0.01 diff --git a/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/index.js b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/index.js new file mode 100644 index 0000000..483f9c7 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/index.js @@ -0,0 +1,17 @@ +import { TypedRequestData } from '@opengsn/common/dist/EIP712/TypedRequestData.js' +import { getHttpClient } from '@opengsn/common/dist/HttpClient.js' +import { ethers } from 'ethers' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// TODO: Query RelayHub for relay registrations +// TODO: Validate each relay (version, network, balance, fees) +// TODO: Select the first valid relay +// TODO: Use it for gasless transfer + +async function main() { + console.log('TODO: Implement relay discovery') +} + +main().catch(console.error) diff --git a/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/package.json b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/package.json new file mode 100644 index 0000000..a55862c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_files/package.json @@ -0,0 +1 @@ +{ "name": "gasless-advanced", "type": "module", "version": "1.0.0", "scripts": { "discover": "node --watch index.js", "optimized": "node --watch index.js" }, "dependencies": { "@opengsn/common": "^2.2.6", "axios": "^1.6.0", "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_solution/index.js b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_solution/index.js new file mode 100644 index 0000000..fc45fcf --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_solution/index.js @@ -0,0 +1,294 @@ +import { HttpClient, HttpWrapper } from '@opengsn/common' +import { TypedRequestData } from '@opengsn/common/dist/EIP712/TypedRequestData.js' +import { ethers } from 'ethers' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// Mainnet addresses +const POLYGON_RPC_URL = 'https://polygon-rpc.com' +const USDT_ADDRESS = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F' +const TRANSFER_CONTRACT_ADDRESS = '0x98E69a6927747339d5E543586FC0262112eBe4BD' +const RELAY_HUB_ADDRESS = '0x6C28AfC105e65782D9Ea6F2cA68df84C9e7d750d' +const RECEIVER_ADDRESS = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +const TRANSFER_AMOUNT_USDT = '0.01' +const STATIC_FEE_USDT = '0.01' + +// ABIs +const USDT_ABI = [ + 'function balanceOf(address) view returns (uint256)', + 'function nonces(address) view returns (uint256)', +] + +const TRANSFER_ABI = [ + 'function transferWithApproval(address token, uint256 amount, address target, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)', + 'function getNonce(address) view returns (uint256)', +] + +const RELAY_HUB_ABI = [ + 'event RelayServerRegistered(address indexed relayManager, uint256 baseRelayFee, uint256 pctRelayFee, string relayUrl)', +] + +async function discoverRelays(provider) { + console.log('\n🔍 Discovering relays from RelayHub...') + + const relayHub = new ethers.Contract(RELAY_HUB_ADDRESS, RELAY_HUB_ABI, provider) + const currentBlock = await provider.getBlockNumber() + const LOOKBACK_BLOCKS = 14400 // ~10 hours on Polygon (2 blocks/min * 60 * 10) + + const fromBlock = currentBlock - LOOKBACK_BLOCKS + + console.log(`Scanning blocks ${fromBlock} to ${currentBlock} (~10 hours)...`) + + const events = await relayHub.queryFilter( + relayHub.filters.RelayServerRegistered(), + fromBlock, + currentBlock, + ) + + console.log(`Found ${events.length} relay registration events`) + + // Extract unique relays + const relayMap = new Map() + for (const event of events) { + const { relayManager, baseRelayFee, pctRelayFee, relayUrl } = event.args + relayMap.set(relayUrl, { + url: relayUrl, + manager: relayManager, + baseRelayFee: baseRelayFee.toString(), + pctRelayFee: pctRelayFee.toString(), + }) + } + + const uniqueRelays = Array.from(relayMap.values()) + console.log(`Found ${uniqueRelays.length} unique relay URLs`) + + return uniqueRelays +} + +async function validateRelay(relay, provider) { + try { + // Ping relay with timeout + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 10000) + + const response = await fetch(`${relay.url}/getaddr`, { + signal: controller.signal, + }) + + clearTimeout(timeout) + + if (!response.ok) + return null + + const relayInfo = await response.json() + + // Check version (must be 2.x) + if (!relayInfo.version || !relayInfo.version.startsWith('2.')) { + console.log(` ❌ ${relay.url} - wrong version: ${relayInfo.version}`) + return null + } + + // Check network (must be Polygon mainnet) + if (relayInfo.networkId !== '137' && relayInfo.chainId !== '137') { + console.log(` ❌ ${relay.url} - wrong network: ${relayInfo.networkId || relayInfo.chainId}`) + return null + } + + // Check ready status + if (!relayInfo.ready) { + console.log(` ❌ ${relay.url} - not ready`) + return null + } + + // Check worker balance + const workerBalance = await provider.getBalance(relayInfo.relayWorkerAddress) + const minBalance = ethers.utils.parseEther('0.01') // 0.01 POL minimum + + if (workerBalance.lt(minBalance)) { + console.log(` ❌ ${relay.url} - low balance: ${ethers.utils.formatEther(workerBalance)} POL`) + return null + } + + // Check fee limits (max 70% percentage, 0 base fee) + const pctFee = Number.parseInt(relay.pctRelayFee) + const baseFee = ethers.BigNumber.from(relay.baseRelayFee) + + if (pctFee > 70) { + console.log(` ❌ ${relay.url} - fee too high: ${pctFee}%`) + return null + } + + if (baseFee.gt(0)) { + console.log(` ❌ ${relay.url} - base fee not acceptable: ${baseFee.toString()}`) + return null + } + + console.log(` ✅ ${relay.url} - valid (${pctFee}% fee, ${ethers.utils.formatEther(workerBalance)} POL)`) + + return { + ...relay, + relayWorkerAddress: relayInfo.relayWorkerAddress, + minGasPrice: relayInfo.minGasPrice, + version: relayInfo.version, + } + } + catch (error) { + console.log(` ❌ ${relay.url} - ${error.message}`) + return null + } +} + +async function findBestRelay(provider) { + const relays = await discoverRelays(provider) + + console.log('\n🔬 Validating relays...') + + for (const relay of relays) { + const validRelay = await validateRelay(relay, provider) + if (validRelay) { + return validRelay + } + } + + throw new Error('No valid relays found') +} + +async function main() { + console.log('🚀 Gasless USDT transfer with relay discovery...\n') + + const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) + const wallet = new ethers.Wallet(PRIVATE_KEY, provider) + + console.log('🔑 Sender:', wallet.address) + console.log('📍 Receiver:', RECEIVER_ADDRESS) + + // Discover and validate relays + const relay = await findBestRelay(provider) + + console.log('\n✅ Using relay:', relay.url) + console.log(' Worker:', relay.relayWorkerAddress) + console.log(' Fee:', `${relay.pctRelayFee}% + ${relay.baseRelayFee} wei`) + + // Rest of the gasless transaction logic (same as lesson 4) + const usdt = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider) + const usdtNonce = await usdt.nonces(wallet.address) + + const transferAmount = ethers.utils.parseUnits(TRANSFER_AMOUNT_USDT, 6) + const feeAmount = ethers.utils.parseUnits(STATIC_FEE_USDT, 6) + const approvalAmount = transferAmount.add(feeAmount) + + // Sign USDT approval + const approveFunctionSignature = usdt.interface.encodeFunctionData('approve', [ + TRANSFER_CONTRACT_ADDRESS, + approvalAmount, + ]) + + const usdtDomain = { + name: 'USDT0', + version: '1', + verifyingContract: USDT_ADDRESS, + salt: ethers.utils.hexZeroPad(ethers.utils.hexlify(137), 32), + } + + const usdtTypes = { + MetaTransaction: [ + { name: 'nonce', type: 'uint256' }, + { name: 'from', type: 'address' }, + { name: 'functionSignature', type: 'bytes' }, + ], + } + + const usdtMessage = { + nonce: usdtNonce.toNumber(), + from: wallet.address, + functionSignature: approveFunctionSignature, + } + + const approvalSignature = await wallet._signTypedData(usdtDomain, usdtTypes, usdtMessage) + const { r: sigR, s: sigS, v: sigV } = ethers.utils.splitSignature(approvalSignature) + + // Build transfer calldata + const transferContract = new ethers.Contract(TRANSFER_CONTRACT_ADDRESS, TRANSFER_ABI, provider) + const transferCalldata = transferContract.interface.encodeFunctionData('transferWithApproval', [ + USDT_ADDRESS, + transferAmount, + RECEIVER_ADDRESS, + feeAmount, + approvalAmount, + sigR, + sigS, + sigV, + ]) + + // Build relay request + const forwarderNonce = await transferContract.getNonce(wallet.address) + const currentBlock = await provider.getBlockNumber() + const validUntil = currentBlock + (2 * 60 * 2) // 2 hours + + const relayRequest = { + request: { + from: wallet.address, + to: TRANSFER_CONTRACT_ADDRESS, + value: '0', + gas: '350000', + nonce: forwarderNonce.toString(), + data: transferCalldata, + validUntil: validUntil.toString(), + }, + relayData: { + gasPrice: '100000000000', // 100 gwei + pctRelayFee: relay.pctRelayFee, + baseRelayFee: relay.baseRelayFee, + relayWorker: relay.relayWorkerAddress, + paymaster: TRANSFER_CONTRACT_ADDRESS, + forwarder: TRANSFER_CONTRACT_ADDRESS, + paymasterData: '0x', + clientId: '1', + }, + } + + // Sign relay request + const forwarderDomain = { + name: 'Forwarder', + version: '1', + chainId: 137, + verifyingContract: TRANSFER_CONTRACT_ADDRESS, + } + + const { types, domain, primaryType, message } = new TypedRequestData( + forwarderDomain.chainId.toString(), + forwarderDomain.verifyingContract, + relayRequest, + ) + + const relaySignature = await wallet._signTypedData(domain, types, message) + + // Submit to relay + console.log('\n📡 Submitting to relay...') + const relayNonce = await provider.getTransactionCount(relay.relayWorkerAddress) + + const httpClient = new HttpClient(new HttpWrapper(), console) + const relayResponse = await httpClient.relayTransaction(relay.url, { + relayRequest, + metadata: { + signature: relaySignature, + approvalData: '0x', + relayHubAddress: RELAY_HUB_ADDRESS, + relayMaxNonce: relayNonce + 3, + }, + }) + + const txHash = typeof relayResponse === 'string' + ? relayResponse + : relayResponse.signedTx || relayResponse.txHash + + console.log('\n✅ Gasless transaction sent!') + console.log('🔗 View:', `https://polygonscan.com/tx/${txHash}`) + console.log('\n💡 Relay discovered dynamically - no hardcoded URLs!') +} + +main().catch((error) => { + console.error('\n❌ Error:', error.message) +}) diff --git a/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_solution/package.json b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_solution/package.json new file mode 100644 index 0000000..a55862c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/_solution/package.json @@ -0,0 +1 @@ +{ "name": "gasless-advanced", "type": "module", "version": "1.0.0", "scripts": { "discover": "node --watch index.js", "optimized": "node --watch index.js" }, "dependencies": { "@opengsn/common": "^2.2.6", "axios": "^1.6.0", "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/6-gasless-transfers/5-relay-discovery/content.md b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/content.md new file mode 100644 index 0000000..e20a14b --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/5-relay-discovery/content.md @@ -0,0 +1,180 @@ +--- +type: lesson +title: "Discovering Relays Dynamically" +focus: /index.js +mainCommand: npm run discover +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Discovering Relays Dynamically + +Hardcoding a relay URL works for demos, but production code needs to discover healthy relays automatically. In this lesson you will query the OpenGSN RelayHub contract, vet the results, and pick a relay that is ready to carry your transaction. The approach mirrors what the Nimiq wallet uses in production. + +--- + +## Objectives + +By the end of this lesson you will: + +- Query on-chain events with ethers.js. +- Check each relay's advertised metadata and on-chain balances. +- Filter out relays that are offline, outdated, or underfunded. +- Produce a resilient fallback chain when the preferred relay fails. + +Before you start, skim the reference material so the field names feel familiar: + +- [RelayHub events in the OpenGSN docs](https://docs.opengsn.org/contracts/relay-hub.html). +- Nimiq’s [gasless transfer architecture notes](https://developers.nimiq.com/). +- Polygon’s [gasless transaction guidelines](https://docs.polygon.technology/). + +--- + +## Step 1: Pull Recent Relay Registrations + +RelayHub emits a `RelayServerRegistered` event whenever a relay announces itself. Scan the recent blocks to collect candidates. + +```js title="discover-relays.ts" showLineNumbers mark=6-24 +const RELAY_HUB_ABI = ['event RelayServerRegistered(address indexed relayManager, uint256 baseRelayFee, uint256 pctRelayFee, string relayUrl)'] +const relayHub = new ethers.Contract(RELAY_HUB_ADDRESS, RELAY_HUB_ABI, provider) + +const currentBlock = await provider.getBlockNumber() +const LOOKBACK_BLOCKS = 14400 // ~10 hours on Polygon + +const events = await relayHub.queryFilter( + relayHub.filters.RelayServerRegistered(), + currentBlock - LOOKBACK_BLOCKS, + currentBlock +) + +const seen = new Map() + +for (const event of events) { + const { relayManager, baseRelayFee, pctRelayFee, relayUrl } = event.args + if (!seen.has(relayUrl)) { + seen.set(relayUrl, { + url: relayUrl, + relayManager, + baseRelayFee, + pctRelayFee, + }) + } +} + +const candidates = Array.from(seen.values()) +console.log(`Found ${candidates.length} unique relay URLs`) +``` + +Looking back roughly 10 hours balances freshness with performance. Adjust the window if you need more or fewer candidates. + +--- + +## Step 2: Ping and Validate Each Relay + +For every registration, call the `/getaddr` endpoint and run a series of health checks before trusting it. + +```js title="validate-relay.ts" showLineNumbers mark=6-29 +async function validateRelay(relay, provider) { + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 10_000) + + const response = await fetch(`${relay.url}/getaddr`, { signal: controller.signal }) + + clearTimeout(timeout) + + if (!response.ok) + return null + + const relayInfo = await response.json() + + if (!relayInfo.version?.startsWith('2.')) + return null + if (relayInfo.networkId !== '137' && relayInfo.chainId !== '137') + return null + if (!relayInfo.ready) + return null + + const workerBalance = await provider.getBalance(relayInfo.relayWorkerAddress) + if (workerBalance.lt(ethers.utils.parseEther('0.01'))) + return null + + const pctFee = Number.parseInt(relay.pctRelayFee) + const baseFee = ethers.BigNumber.from(relay.baseRelayFee) + + if (pctFee > 70 || baseFee.gt(0)) + return null + + return { + ...relay, + relayWorkerAddress: relayInfo.relayWorkerAddress, + minGasPrice: relayInfo.minGasPrice, + version: relayInfo.version, + } + } + catch (error) { + return null // Relay offline or invalid + } +} +``` + +`AbortController` gives us a portable timeout without extra dependencies, which keeps the sample compatible with both Node.js scripts and browser bundlers. + +Checks to keep in mind: + +- **Version** must start with 2.x to match the OpenGSN v2 protocol. +- **Network ID** should be 137 for Polygon mainnet. +- **Worker balance** needs enough POL to front your transaction (the example uses 0.01 POL as a floor). +- **Readiness flag** confirms the relay advertises itself as accepting requests. +- **Fee caps** ensure you never accept a base fee or a percentage beyond your policy. + +--- + +## Step 3: Select the First Healthy Relay + +Iterate through the registrations until you find one that passes validation. You can collect alternates for fallback if desired. + +```js title="find-best-relay.ts" showLineNumbers mark=3-14 +async function findBestRelay(provider) { + console.log('\n🔬 Validating relays...') + + for (const relay of candidates) { + const validRelay = await validateRelay(relay, provider) + if (validRelay) + return validRelay + } + + throw new Error('No valid relays found') +} + +const relay = await findBestRelay(provider) +console.log('✅ Using relay:', relay.url) +``` + +This simple loop already improves reliability dramatically compared to a hardcoded URL. + +--- + +## Why This Beats a Static Relay + +- ✅ Automatically skips relays that are offline or misconfigured. +- ✅ Picks up newly registered relays without code changes. +- ✅ Gives you hooks to rank relays by price, latency, or trust level. +- ❌ Still relies on static fee estimates (we will tackle that in the next lesson). + +--- + +## Wrap-Up + +You now have a discovery pipeline that: + +- ✅ Queries RelayHub for fresh relay registrations. +- ✅ Validates each relay's network, version, balance, and responsiveness. +- ✅ Falls back gracefully when a relay fails health checks. +- ✅ Removes the last hardcoded relay URL from your workflow. + +Next up: **Lesson 5** where you replace static fees with a dynamic, production-ready calculation. diff --git a/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/.env.example b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/.env.example new file mode 100644 index 0000000..a1ca84c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/.env.example @@ -0,0 +1,4 @@ +POLYGON_RPC_URL=https://polygon-rpc.com +SPONSOR_PRIVATE_KEY= +RECEIVER_ADDRESS=0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184 +TRANSFER_AMOUNT_USDT=0.01 diff --git a/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/index.js b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/index.js new file mode 100644 index 0000000..39a61a5 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/index.js @@ -0,0 +1,21 @@ +import { TypedRequestData } from '@opengsn/common/dist/EIP712/TypedRequestData.js' +import { getHttpClient } from '@opengsn/common/dist/HttpClient.js' +import { ethers } from 'ethers' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// TODO: Implement relay discovery +// TODO: Calculate optimized fees for each relay: +// - Get network gas price and relay minimum +// - Apply safety buffers (10% mainnet, 25% testnet) +// - Calculate POL cost with relay percentage fee +// - Convert to USDT with safety margin +// TODO: Compare relays and select the cheapest +// TODO: Use optimized fee for gasless transfer + +async function main() { + console.log('TODO: Implement optimized fee calculation') +} + +main().catch(console.error) diff --git a/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/package.json b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/package.json new file mode 100644 index 0000000..a55862c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_files/package.json @@ -0,0 +1 @@ +{ "name": "gasless-advanced", "type": "module", "version": "1.0.0", "scripts": { "discover": "node --watch index.js", "optimized": "node --watch index.js" }, "dependencies": { "@opengsn/common": "^2.2.6", "axios": "^1.6.0", "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_solution/index.js b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_solution/index.js new file mode 100644 index 0000000..e5b0d31 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_solution/index.js @@ -0,0 +1,378 @@ +import { HttpClient, HttpWrapper } from '@opengsn/common' +import { TypedRequestData } from '@opengsn/common/dist/EIP712/TypedRequestData.js' +import { ethers } from 'ethers' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// Mainnet addresses +const POLYGON_RPC_URL = 'https://polygon-rpc.com' +const USDT_ADDRESS = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F' +const TRANSFER_CONTRACT_ADDRESS = '0x98E69a6927747339d5E543586FC0262112eBe4BD' +const RELAY_HUB_ADDRESS = '0x6C28AfC105e65782D9Ea6F2cA68df84C9e7d750d' +const RECEIVER_ADDRESS = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +const TRANSFER_AMOUNT_USDT = '0.01' + +// Uniswap V3 addresses for price queries +const UNISWAP_QUOTER = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6' +const WMATIC_ADDRESS = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' +const USDT_WMATIC_POOL = '0x9B08288C3Be4F62bbf8d1C20Ac9C5e6f9467d8B7' + +// Method selector for transferWithApproval +const METHOD_SELECTOR_TRANSFER_WITH_APPROVAL = '0x8d89149b' + +// ABIs +const USDT_ABI = [ + 'function balanceOf(address) view returns (uint256)', + 'function nonces(address) view returns (uint256)', +] + +const TRANSFER_ABI = [ + 'function transferWithApproval(address token, uint256 amount, address target, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)', + 'function getNonce(address) view returns (uint256)', + 'function getRequiredRelayGas(bytes4 methodId) view returns (uint256)', +] + +const RELAY_HUB_ABI = [ + 'event RelayServerRegistered(address indexed relayManager, uint256 baseRelayFee, uint256 pctRelayFee, string relayUrl)', +] + +const UNISWAP_POOL_ABI = [ + 'function fee() external view returns (uint24)', +] + +const UNISWAP_QUOTER_ABI = [ + 'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)', +] + +async function discoverRelays(provider) { + const relayHub = new ethers.Contract(RELAY_HUB_ADDRESS, RELAY_HUB_ABI, provider) + const currentBlock = await provider.getBlockNumber() + const LOOKBACK_BLOCKS = 1600 // ~1 hour (2s per block) + + const events = await relayHub.queryFilter( + relayHub.filters.RelayServerRegistered(), + currentBlock - LOOKBACK_BLOCKS, + currentBlock, + ) + + const relayMap = new Map() + for (const event of events) { + const { relayManager, baseRelayFee, pctRelayFee, relayUrl } = event.args + relayMap.set(relayUrl, { + url: relayUrl, + manager: relayManager, + baseRelayFee, + pctRelayFee: pctRelayFee.toNumber(), + }) + } + + return Array.from(relayMap.values()) +} + +async function validateRelay(relay, provider) { + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 10000) + + const response = await fetch(`${relay.url}/getaddr`, { signal: controller.signal }) + clearTimeout(timeout) + + if (!response.ok) + return null + + const relayInfo = await response.json() + + // Basic validation + if (!relayInfo.version?.startsWith('2.')) + return null + if (relayInfo.networkId !== '137' && relayInfo.chainId !== '137') + return null + if (!relayInfo.ready) + return null + + const workerBalance = await provider.getBalance(relayInfo.relayWorkerAddress) + if (workerBalance.lt(ethers.utils.parseEther('0.01'))) + return null + + // Fee validation + if (relay.pctRelayFee > 70) + return null + if (relay.baseRelayFee.gt(0)) + return null + + return { + ...relay, + relayWorkerAddress: relayInfo.relayWorkerAddress, + minGasPrice: ethers.BigNumber.from(relayInfo.minGasPrice || 0), + version: relayInfo.version, + } + } + catch { + return null + } +} + +async function getPolUsdtPrice(provider) { + // Query Uniswap V3 pool for USDT/WMATIC price + const pool = new ethers.Contract(USDT_WMATIC_POOL, UNISWAP_POOL_ABI, provider) + const quoter = new ethers.Contract(UNISWAP_QUOTER, UNISWAP_QUOTER_ABI, provider) + + // Get pool fee tier + const fee = await pool.fee() + + // Quote: How much POL for 1 USDT (1_000_000 base units)? + const polAmountOut = await quoter.callStatic.quoteExactInputSingle( + USDT_ADDRESS, // tokenIn (USDT) + WMATIC_ADDRESS, // tokenOut (WMATIC) + fee, // pool fee + ethers.utils.parseUnits('1', 6), // 1 USDT + 0, // sqrtPriceLimitX96 + ) + + return polAmountOut // POL wei per 1 USDT +} + +async function calculateOptimalFee(relay, provider, transferContract, isMainnet = true) { + // Step 1: Get network gas price + const networkGasPrice = await provider.getGasPrice() + + console.log(' Network gas price:', ethers.utils.formatUnits(networkGasPrice, 'gwei'), 'gwei') + console.log(' Relay min gas price:', ethers.utils.formatUnits(relay.minGasPrice, 'gwei'), 'gwei') + + // Step 2: Take max of network and relay minimum + const baseGasPrice = networkGasPrice.gt(relay.minGasPrice) ? networkGasPrice : relay.minGasPrice + + // Step 3: Apply buffer (10% mainnet, 25% testnet) + const bufferPercentage = isMainnet ? 110 : 125 + const bufferedGasPrice = baseGasPrice.mul(bufferPercentage).div(100) + + console.log(' Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'gwei'), 'gwei', `(${bufferPercentage}%)`) + + // Step 4: Get gas limit from transfer contract + const gasLimit = await transferContract.getRequiredRelayGas(METHOD_SELECTOR_TRANSFER_WITH_APPROVAL) + + console.log(' Gas limit:', gasLimit.toString()) + + // Step 5: Calculate base cost + const baseCost = bufferedGasPrice.mul(gasLimit) + + // Step 6: Apply relay percentage fee + const costWithPctFee = baseCost.mul(100 + relay.pctRelayFee).div(100) + + // Step 7: Add base relay fee + const totalPOLCost = costWithPctFee.add(relay.baseRelayFee) + + console.log(' Total POL cost:', ethers.utils.formatEther(totalPOLCost), 'POL') + console.log(' Relay fee:', `${relay.pctRelayFee}%`) + + // Step 8: Get real-time POL/USDT price from Uniswap + const polPerUsdt = await getPolUsdtPrice(provider) + console.log(' Uniswap rate:', ethers.utils.formatEther(polPerUsdt), 'POL per USDT') + + // Step 9: Convert POL fee to USDT with 10% buffer + // totalPOLCost (POL wei) / polPerUsdt (POL wei per USDT) = USDT base units + const feeInUSDT = totalPOLCost.mul(1_000_000).div(polPerUsdt).mul(110).div(100) + + console.log(' USDT fee:', ethers.utils.formatUnits(feeInUSDT, 6), 'USDT') + + return { + usdtFee: feeInUSDT, + gasPrice: bufferedGasPrice, + gasLimit, + polCost: totalPOLCost, + } +} + +async function findBestRelay(provider, transferContract) { + console.log('\n🔍 Discovering relays...') + const relays = await discoverRelays(provider) + console.log(`Found ${relays.length} unique relay URLs`) + + console.log('\n🔬 Validating and calculating fees...\n') + + let bestRelay = null + let lowestFee = ethers.constants.MaxUint256 + + for (const relay of relays) { + const validRelay = await validateRelay(relay, provider) + + if (!validRelay) + continue + + console.log(`📊 ${relay.url}`) + + try { + const feeData = await calculateOptimalFee(validRelay, provider, transferContract) + + if (feeData.usdtFee.lt(lowestFee)) { + lowestFee = feeData.usdtFee + bestRelay = { ...validRelay, feeData } + console.log(' ✅ New best relay!\n') + } + else { + console.log(' ⚪ Not the cheapest\n') + } + } + catch (error) { + console.log(' ❌ Fee calculation failed:', error.message, '\n') + } + } + + if (!bestRelay) { + throw new Error('No valid relays found') + } + + return bestRelay +} + +async function main() { + console.log('🚀 Gasless USDT transfer with optimized fee calculation...\n') + + const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) + const wallet = new ethers.Wallet(PRIVATE_KEY, provider) + + console.log('🔑 Sender:', wallet.address) + console.log('📍 Receiver:', RECEIVER_ADDRESS) + + // Setup transfer contract + const transferContract = new ethers.Contract(TRANSFER_CONTRACT_ADDRESS, TRANSFER_ABI, provider) + + // Find best relay with optimized fee + const relay = await findBestRelay(provider, transferContract) + + console.log('\n✅ Selected relay:', relay.url) + console.log(' Worker:', relay.relayWorkerAddress) + console.log(' Optimized USDT fee:', ethers.utils.formatUnits(relay.feeData.usdtFee, 6), 'USDT') + console.log(' Gas price:', ethers.utils.formatUnits(relay.feeData.gasPrice, 'gwei'), 'gwei') + + // Build transaction with calculated fee + const usdt = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider) + const usdtNonce = await usdt.nonces(wallet.address) + + const transferAmount = ethers.utils.parseUnits(TRANSFER_AMOUNT_USDT, 6) + const feeAmount = relay.feeData.usdtFee // Use optimized fee + const approvalAmount = transferAmount.add(feeAmount) + + console.log('\n💰 Transfer:', TRANSFER_AMOUNT_USDT, 'USDT') + console.log('💸 Optimized fee:', ethers.utils.formatUnits(feeAmount, 6), 'USDT') + console.log('✅ Total approval:', ethers.utils.formatUnits(approvalAmount, 6), 'USDT') + + // Sign USDT approval + const approveFunctionSignature = usdt.interface.encodeFunctionData('approve', [ + TRANSFER_CONTRACT_ADDRESS, + approvalAmount, + ]) + + const usdtDomain = { + name: 'USDT0', + version: '1', + verifyingContract: USDT_ADDRESS, + salt: ethers.utils.hexZeroPad(ethers.utils.hexlify(137), 32), + } + + const usdtTypes = { + MetaTransaction: [ + { name: 'nonce', type: 'uint256' }, + { name: 'from', type: 'address' }, + { name: 'functionSignature', type: 'bytes' }, + ], + } + + const usdtMessage = { + nonce: usdtNonce.toNumber(), + from: wallet.address, + functionSignature: approveFunctionSignature, + } + + const approvalSignature = await wallet._signTypedData(usdtDomain, usdtTypes, usdtMessage) + const { r: sigR, s: sigS, v: sigV } = ethers.utils.splitSignature(approvalSignature) + + // Build transfer calldata + const transferCalldata = transferContract.interface.encodeFunctionData('transferWithApproval', [ + USDT_ADDRESS, + transferAmount, + RECEIVER_ADDRESS, + feeAmount, + approvalAmount, + sigR, + sigS, + sigV, + ]) + + // Build relay request with optimized gas price + const forwarderNonce = await transferContract.getNonce(wallet.address) + const currentBlock = await provider.getBlockNumber() + const validUntil = currentBlock + (2 * 60 * 2) // 2 hours + + const relayRequest = { + request: { + from: wallet.address, + to: TRANSFER_CONTRACT_ADDRESS, + value: '0', + gas: relay.feeData.gasLimit.toString(), + nonce: forwarderNonce.toString(), + data: transferCalldata, + validUntil: validUntil.toString(), + }, + relayData: { + gasPrice: relay.feeData.gasPrice.toString(), + pctRelayFee: relay.pctRelayFee.toString(), + baseRelayFee: relay.baseRelayFee.toString(), + relayWorker: relay.relayWorkerAddress, + paymaster: TRANSFER_CONTRACT_ADDRESS, + forwarder: TRANSFER_CONTRACT_ADDRESS, + paymasterData: '0x', + clientId: '1', + }, + } + + // Sign relay request + const forwarderDomain = { + name: 'Forwarder', + version: '1', + chainId: 137, + verifyingContract: TRANSFER_CONTRACT_ADDRESS, + } + + const { types, domain, primaryType, message } = new TypedRequestData( + forwarderDomain.chainId.toString(), + forwarderDomain.verifyingContract, + relayRequest, + ) + + const relaySignature = await wallet._signTypedData(domain, types, message) + + // Submit to relay + console.log('\n📡 Submitting to relay...') + const relayNonce = await provider.getTransactionCount(relay.relayWorkerAddress) + + const httpClient = new HttpClient(new HttpWrapper(), console) + const relayResponse = await httpClient.relayTransaction(relay.url, { + relayRequest, + metadata: { + signature: relaySignature, + approvalData: '0x', + relayHubAddress: RELAY_HUB_ADDRESS, + relayMaxNonce: relayNonce + 3, + }, + }) + + const txHash = typeof relayResponse === 'string' + ? relayResponse + : relayResponse.signedTx || relayResponse.txHash + + console.log('\n✅ Gasless transaction sent!') + console.log('🔗 View:', `https://polygonscan.com/tx/${txHash}`) + console.log('\n💡 Used production-grade fee optimization:') + console.log(' ✅ Dynamic gas price discovery') + console.log(' ✅ Gas limits from contract (getRequiredRelayGas)') + console.log(' ✅ Real-time POL/USDT pricing from Uniswap V3') + console.log(' ✅ Network-aware safety buffers') + console.log(' ✅ Relay fee comparison') +} + +main().catch((error) => { + console.error('\n❌ Error:', error.message) +}) diff --git a/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_solution/package.json b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_solution/package.json new file mode 100644 index 0000000..a55862c --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/_solution/package.json @@ -0,0 +1 @@ +{ "name": "gasless-advanced", "type": "module", "version": "1.0.0", "scripts": { "discover": "node --watch index.js", "optimized": "node --watch index.js" }, "dependencies": { "@opengsn/common": "^2.2.6", "axios": "^1.6.0", "ethers": "^5.7.2" } } diff --git a/src/content/tutorial/6-gasless-transfers/6-optimized-fees/content.md b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/content.md new file mode 100644 index 0000000..4ed52c5 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/6-optimized-fees/content.md @@ -0,0 +1,294 @@ +--- +type: lesson +title: "Optimized Fee Calculation" +focus: /index.js +mainCommand: npm run optimized +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# Optimized Fee Calculation + +Hardcoding relay fees works for prototypes, but production systems need to adapt to market conditions in real time. In this lesson you will calculate the exact fee a relay should receive based on live gas prices, relay-specific pricing, and protective buffers - mirroring the logic in the Nimiq wallet. + +--- + +## Learning Goals + +- Fetch the current network gas price and respect the relay's minimum. +- Apply context-aware buffers that keep transactions reliable without overspending. +- Combine relay percentage fees and base fees into a single POL amount. +- Convert that POL cost into USDT using conservative pricing assumptions. +- Compare multiple relays and pick the most cost-effective option. + +The maths mirrors the `calculateGaslessFee` helper inside `@cashlink/currency/src/gasless/fees.ts`. Cross-reference it with the [OpenGSN fee model documentation](https://docs.opengsn.org/relay/relay-lifecycle.html#fees) and the [Nimiq wallet engineering notes](https://developers.nimiq.com/) if you want to see the production lineage. + +--- + +## The Core Formula + +```text title="Fee formula" +chainTokenFee = (gasPrice * gasLimit * (1 + pctRelayFee/100)) + baseRelayFee +usdtFee = (chainTokenFee / usdtPriceInPOL) * safetyBuffer +``` + +Where: + +- `gasPrice` is the higher of the network price and the relay's minimum, optionally buffered. +- `gasLimit` depends on the method you are executing. +- `pctRelayFee` and `baseRelayFee` come from the relay registration event. +- `safetyBuffer` adds wiggle room (typically 10-50%). +- `usdtPriceInPOL` converts the POL cost into USDT. + +--- + +## Step 1: Read the Network Gas Price + +```js title="gas-price.ts" showLineNumbers mark=1-8 +const networkGasPrice = await provider.getGasPrice() +console.log('Network gas price:', ethers.utils.formatUnits(networkGasPrice, 'gwei'), 'gwei') + +// Get relay's minimum +const relay = await discoverRelay() // From lesson 3 +const minGasPrice = ethers.BigNumber.from(relay.minGasPrice) + +// Take the max +const gasPrice = networkGasPrice.gt(minGasPrice) ? networkGasPrice : minGasPrice +``` + +Using the maximum of the two ensures you never pay less than the relay requires, while still benefiting from low network prices when possible. + +Why the **min gas price** matters: each relay advertises a floor in its `/getaddr` response. If you submit a transaction below that price the worker will reject it. The [Polygon gas market guide](https://docs.polygon.technology/docs/develop/network-details/gas/) explains why congestion spikes make this floor fluctuate. + +--- + +## Step 2: Apply a Safety Buffer + +Different workflows tolerate risk differently. Adjust the buffer based on the method or environment you are in. + +```js title="buffer.ts" showLineNumbers mark=1-13 +const ENV_MAIN = true // Set based on environment + +let bufferPercentage +if (method === 'redeemWithSecretInData') { + bufferPercentage = 150 // 50% buffer (swap fee volatility) +} +else if (ENV_MAIN) { + bufferPercentage = 110 // 10% buffer (mainnet) +} +else { + bufferPercentage = 125 // 25% buffer (testnet, more volatile) +} + +const bufferedGasPrice = gasPrice.mul(bufferPercentage).div(100) +console.log('Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'gwei'), 'gwei') +``` + +--- + +## Step 3: Get Gas Limit from Transfer Contract + +Instead of hardcoding gas limits, query the transfer contract directly: + +```js title="gas-limit.ts" showLineNumbers mark=1-11 +const TRANSFER_CONTRACT_ABI = [ + 'function getRequiredRelayGas(bytes4 methodId) view returns (uint256)' +] + +const transferContract = new ethers.Contract( + TRANSFER_CONTRACT_ADDRESS, + TRANSFER_CONTRACT_ABI, + provider +) + +// Method selector for transferWithApproval +const METHOD_SELECTOR = '0x8d89149b' +const gasLimit = await transferContract.getRequiredRelayGas(METHOD_SELECTOR) + +console.log('Gas limit:', gasLimit.toString()) +``` + +This ensures your gas estimates stay accurate even if the contract changes. + +--- + +## Step 4: Combine Gas Costs and Relay Fees + +```js title="fee-components.ts" showLineNumbers mark=1-14 +// Calculate base cost +const baseCost = bufferedGasPrice.mul(gasLimit) + +// Add relay percentage fee +const pctRelayFee = 15 // From relay registration (e.g., 15%) +const costWithPct = baseCost.mul(100 + pctRelayFee).div(100) + +// Add base relay fee +const baseRelayFee = ethers.BigNumber.from(relay.baseRelayFee) +const totalChainTokenFee = costWithPct.add(baseRelayFee) + +console.log('Total POL cost:', ethers.utils.formatEther(totalChainTokenFee)) +``` + +This yields the amount of POL the relay expects to receive after covering gas. + +--- + +## Step 5: Get Real-Time POL/USDT Price from Uniswap + +Query Uniswap V3 for the current exchange rate: + +```js title="uniswap-price.ts" showLineNumbers mark=1-21 +const UNISWAP_QUOTER = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6' +const WMATIC = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' +const USDT_WMATIC_POOL = '0x9B08288C3Be4F62bbf8d1C20Ac9C5e6f9467d8B7' + +const POOL_ABI = ['function fee() external view returns (uint24)'] +const QUOTER_ABI = [ + 'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)' +] + +async function getPolUsdtPrice(provider) { + const pool = new ethers.Contract(USDT_WMATIC_POOL, POOL_ABI, provider) + const quoter = new ethers.Contract(UNISWAP_QUOTER, QUOTER_ABI, provider) + + const fee = await pool.fee() + + // Quote: How much POL for 1 USDT? + const polPerUsdt = await quoter.callStatic.quoteExactInputSingle( + USDT_ADDRESS, WMATIC, fee, ethers.utils.parseUnits('1', 6), 0 + ) + + return polPerUsdt // POL wei per 1 USDT +} +``` + +--- + +## Step 6: Convert POL Cost to USDT + +```js title="fee-to-usdt.ts" showLineNumbers mark=1-8 +const polPerUsdt = await getPolUsdtPrice(provider) + +// Convert: totalPOLCost / polPerUsdt = USDT units +// Apply 10% buffer for safety +const feeInUSDT = totalChainTokenFee + .mul(1_000_000) + .div(polPerUsdt) + .mul(110).div(100) + +console.log('USDT fee:', ethers.utils.formatUnits(feeInUSDT, 6)) +``` + +Using Uniswap ensures your fees reflect current market rates, preventing underpayment when POL appreciates. + +--- + +## Step 7: Reject Expensive Relays + +```js title="guardrails.ts" showLineNumbers mark=1-9 +const MAX_PCT_RELAY_FEE = 70 // Never accept >70% +const MAX_BASE_RELAY_FEE = 0 // Never accept base fee + +if (pctRelayFee > MAX_PCT_RELAY_FEE) { + throw new Error(`Relay fee too high: ${pctRelayFee}%`) +} + +if (baseRelayFee.gt(MAX_BASE_RELAY_FEE)) { + throw new Error('Relay base fee not acceptable') +} +``` + +These guardrails prevent accidental overpayment when a relay is misconfigured or opportunistic. + +--- + +## Step 8: Choose the Best Relay + +```js title="choose-relay.ts" showLineNumbers mark=1-17 +async function getBestRelay(relays, gasLimit, method) { + let bestRelay = null + let lowestFee = ethers.constants.MaxUint256 + + for (const relay of relays) { + try { + const fee = await calculateFee(relay, gasLimit, method) + if (fee.lt(lowestFee)) { + lowestFee = fee + bestRelay = relay + } + } + catch (error) { + continue // Skip invalid relays + } + } + + return bestRelay +} +``` + +When you have multiple candidates, this loop compares their fees and picks the cheapest valid option. + +Want more than “cheapest wins”? It is common to score relays on latency, historical success rate, or geographic proximity. The [OpenGSN Relay Operator checklist](https://docs.opengsn.org/relay/relay-operator.html) lists the metrics most teams monitor. + +--- + +## Production Considerations + +These extras come straight from the Nimiq wallet codebase: + +1. **Timeout relay requests after two seconds** to avoid hanging the UI. +2. **Cap the number of relays you test** (for example, at 10) to keep discovery fast. +3. **Require worker balances at least twice the expected gas cost** for safety. +4. **Skip inactive relays** unless they belong to trusted providers such as Fastspot. +5. **Use generators or iterators** so you can stop searching the moment a good relay appears. + +--- + +## Putting It All Together + +```js title="calculate-optimal-fee.ts" showLineNumbers mark=1-21 +async function calculateOptimalFee(relay, provider, transferContract) { + // 1. Get gas prices + const networkGasPrice = await provider.getGasPrice() + const baseGasPrice = networkGasPrice.gt(relay.minGasPrice) + ? networkGasPrice + : relay.minGasPrice + + // 2. Apply buffer + const bufferedGasPrice = baseGasPrice.mul(110).div(100) // 10% mainnet + + // 3. Get gas limit from contract + const gasLimit = await transferContract.getRequiredRelayGas('0x8d89149b') + + // 4. Calculate POL fee + const baseCost = bufferedGasPrice.mul(gasLimit) + const withPctFee = baseCost.mul(100 + relay.pctRelayFee).div(100) + const totalPOL = withPctFee.add(relay.baseRelayFee) + + // 5. Convert to USDT via Uniswap + const polPerUsdt = await getPolUsdtPrice(provider) + const usdtFee = totalPOL.mul(1_000_000).div(polPerUsdt).mul(110).div(100) + + return { usdtFee, gasPrice: bufferedGasPrice, gasLimit } +} +``` + +Reuse this helper whenever you prepare a meta-transaction so each request reflects current network conditions. + +--- + +## Wrap-Up + +You now have a production-grade fee engine that: + +- ✅ Tracks live gas prices and relay minimums. +- ✅ Queries contract for accurate gas limits. +- ✅ Uses Uniswap V3 for real-time POL/USDT rates. +- ✅ Applies thoughtful buffers to avoid underpayment. +- ✅ Compares relays and selects the most cost-effective option. + +At this point your gasless transaction pipeline matches the approach we ship in the Nimiq wallet - ready for real users. The next lesson covers USDC transfers using the EIP-2612 permit approval method, showing how different tokens require different approval strategies. diff --git a/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_files/index.js b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_files/index.js new file mode 100644 index 0000000..1f852bb --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_files/index.js @@ -0,0 +1,281 @@ +import { HttpClient, HttpWrapper } from '@opengsn/common' +import { TypedRequestData } from '@opengsn/common/dist/EIP712/TypedRequestData.js' +import { ethers } from 'ethers' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// Mainnet addresses +const POLYGON_RPC_URL = 'https://polygon-rpc.com' +const USDC_ADDRESS = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' +const TRANSFER_CONTRACT_ADDRESS = '0x3157d422cd1be13AC4a7cb00957ed717e648DFf2' +const RELAY_HUB_ADDRESS = '0x6C28AfC105e65782D9Ea6F2cA68df84C9e7d750d' +const RECEIVER_ADDRESS = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +const TRANSFER_AMOUNT_USDC = '0.01' + +// TODO: Add Uniswap V3 addresses +// - UNISWAP_QUOTER: 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6 +// - WMATIC_ADDRESS: 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270 +// - USDC_WMATIC_POOL: 0xA374094527e1673A86dE625aa59517c5dE346d32 + +// TODO: Add method selector for transferWithPermit (0x36efd16f) + +// ABIs +const USDC_ABI = [ + 'function balanceOf(address) view returns (uint256)', + 'function nonces(address) view returns (uint256)', +] + +const TRANSFER_ABI = [ + 'function transferWithPermit(address token, uint256 amount, address target, uint256 fee, uint256 deadline, bytes32 sigR, bytes32 sigS, uint8 sigV)', + 'function getNonce(address) view returns (uint256)', + 'function getRequiredRelayGas(bytes4 methodId) view returns (uint256)', +] + +const RELAY_HUB_ABI = [ + 'event RelayServerRegistered(address indexed relayManager, uint256 baseRelayFee, uint256 pctRelayFee, string relayUrl)', +] + +const UNISWAP_POOL_ABI = [ + 'function fee() external view returns (uint24)', +] + +const UNISWAP_QUOTER_ABI = [ + 'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)', +] + +async function discoverRelays(provider) { + const relayHub = new ethers.Contract(RELAY_HUB_ADDRESS, RELAY_HUB_ABI, provider) + const currentBlock = await provider.getBlockNumber() + const LOOKBACK_BLOCKS = 1600 // ~1 hour + + const events = await relayHub.queryFilter( + relayHub.filters.RelayServerRegistered(), + currentBlock - LOOKBACK_BLOCKS, + currentBlock, + ) + + const relayMap = new Map() + for (const event of events) { + const { relayManager, baseRelayFee, pctRelayFee, relayUrl } = event.args + relayMap.set(relayUrl, { + url: relayUrl, + manager: relayManager, + baseRelayFee, + pctRelayFee: pctRelayFee.toNumber(), + }) + } + + return Array.from(relayMap.values()) +} + +async function validateRelay(relay, provider) { + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 10000) + + const response = await fetch(`${relay.url}/getaddr`, { signal: controller.signal }) + clearTimeout(timeout) + + if (!response.ok) + return null + + const relayInfo = await response.json() + + if (!relayInfo.version?.startsWith('2.')) + return null + if (relayInfo.networkId !== '137' && relayInfo.chainId !== '137') + return null + if (!relayInfo.ready) + return null + + const workerBalance = await provider.getBalance(relayInfo.relayWorkerAddress) + if (workerBalance.lt(ethers.utils.parseEther('0.01'))) + return null + + if (relay.pctRelayFee > 70) + return null + if (relay.baseRelayFee.gt(0)) + return null + + return { + ...relay, + relayWorkerAddress: relayInfo.relayWorkerAddress, + minGasPrice: ethers.BigNumber.from(relayInfo.minGasPrice || 0), + version: relayInfo.version, + } + } + catch { + return null + } +} + +async function getPolUsdcPrice(provider) { + // TODO: Query Uniswap V3 pool for USDC/WMATIC price + // 1. Create pool contract with USDC_WMATIC_POOL + // 2. Create quoter contract with UNISWAP_QUOTER + // 3. Get pool fee + // 4. Call quoter.callStatic.quoteExactInputSingle with: + // - tokenIn: USDC_ADDRESS + // - tokenOut: WMATIC_ADDRESS + // - fee: pool fee + // - amountIn: 1 USDC (1e6) + // - sqrtPriceLimitX96: 0 + // 5. Return POL amount +} + +async function calculateOptimalFee(relay, provider, transferContract, isMainnet = true) { + const networkGasPrice = await provider.getGasPrice() + + console.log(' Network gas price:', ethers.utils.formatUnits(networkGasPrice, 'gwei'), 'gwei') + console.log(' Relay min gas price:', ethers.utils.formatUnits(relay.minGasPrice, 'gwei'), 'gwei') + + const baseGasPrice = networkGasPrice.gt(relay.minGasPrice) ? networkGasPrice : relay.minGasPrice + const bufferPercentage = isMainnet ? 110 : 125 + const bufferedGasPrice = baseGasPrice.mul(bufferPercentage).div(100) + + console.log(' Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'gwei'), 'gwei', `(${bufferPercentage}%)`) + + // TODO: Get gas limit using METHOD_SELECTOR_TRANSFER_WITH_PERMIT + + console.log(' Gas limit:', gasLimit.toString()) + + const baseCost = bufferedGasPrice.mul(gasLimit) + const costWithPctFee = baseCost.mul(100 + relay.pctRelayFee).div(100) + const totalPOLCost = costWithPctFee.add(relay.baseRelayFee) + + console.log(' Total POL cost:', ethers.utils.formatEther(totalPOLCost), 'POL') + console.log(' Relay fee:', `${relay.pctRelayFee}%`) + + // TODO: Get POL/USDC price from Uniswap + + console.log(' Uniswap rate:', ethers.utils.formatEther(polPerUsdc), 'POL per USDC') + + // TODO: Convert POL fee to USDC with 10% buffer + + console.log(' USDC fee:', ethers.utils.formatUnits(feeInUSDC, 6), 'USDC') + + return { + usdcFee: feeInUSDC, + gasPrice: bufferedGasPrice, + gasLimit, + polCost: totalPOLCost, + } +} + +async function findBestRelay(provider, transferContract) { + console.log('\n🔍 Discovering relays...') + const relays = await discoverRelays(provider) + console.log(`Found ${relays.length} unique relay URLs`) + + console.log('\n🔬 Validating and calculating fees...\n') + + let bestRelay = null + let lowestFee = ethers.constants.MaxUint256 + + for (const relay of relays) { + const validRelay = await validateRelay(relay, provider) + + if (!validRelay) + continue + + console.log(`📊 ${relay.url}`) + + try { + const feeData = await calculateOptimalFee(validRelay, provider, transferContract) + + if (feeData.usdcFee.lt(lowestFee)) { + lowestFee = feeData.usdcFee + bestRelay = { ...validRelay, feeData } + console.log(' ✅ New best relay!\n') + } + else { + console.log(' ⚪ Not the cheapest\n') + } + } + catch (error) { + console.log(' ❌ Fee calculation failed:', error.message, '\n') + } + } + + if (!bestRelay) { + throw new Error('No valid relays found') + } + + return bestRelay +} + +async function main() { + console.log('🚀 Gasless USDC transfer with EIP-2612 Permit...\n') + + const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) + const wallet = new ethers.Wallet(PRIVATE_KEY, provider) + + console.log('🔑 Sender:', wallet.address) + console.log('📍 Receiver:', RECEIVER_ADDRESS) + + const transferContract = new ethers.Contract(TRANSFER_CONTRACT_ADDRESS, TRANSFER_ABI, provider) + + const relay = await findBestRelay(provider, transferContract) + + console.log('\n✅ Selected relay:', relay.url) + console.log(' Worker:', relay.relayWorkerAddress) + console.log(' Optimized USDC fee:', ethers.utils.formatUnits(relay.feeData.usdcFee, 6), 'USDC') + console.log(' Gas price:', ethers.utils.formatUnits(relay.feeData.gasPrice, 'gwei'), 'gwei') + + const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, provider) + const usdcNonce = await usdc.nonces(wallet.address) + + const transferAmount = ethers.utils.parseUnits(TRANSFER_AMOUNT_USDC, 6) + const feeAmount = relay.feeData.usdcFee + const approvalAmount = transferAmount.add(feeAmount) + + console.log('\n💰 Transfer:', TRANSFER_AMOUNT_USDC, 'USDC') + console.log('💸 Optimized fee:', ethers.utils.formatUnits(feeAmount, 6), 'USDC') + console.log('✅ Total approval:', ethers.utils.formatUnits(approvalAmount, 6), 'USDC') + + // TODO: Sign EIP-2612 Permit + // Domain: + // name: 'USD Coin' + // version: '2' + // chainId: 137 + // verifyingContract: USDC_ADDRESS + // + // Types: + // Permit: [ + // { name: 'owner', type: 'address' }, + // { name: 'spender', type: 'address' }, + // { name: 'value', type: 'uint256' }, + // { name: 'nonce', type: 'uint256' }, + // { name: 'deadline', type: 'uint256' }, + // ] + // + // Message: + // owner: wallet.address + // spender: TRANSFER_CONTRACT_ADDRESS + // value: approvalAmount + // nonce: usdcNonce.toNumber() + // deadline: ethers.constants.MaxUint256 + + // TODO: Build transfer calldata with transferWithPermit + // Parameters: [token, amount, target, fee, deadline, r, s, v] + + // TODO: Build relay request (same as Lesson 6) + + // TODO: Sign relay request (same as Lesson 6) + + // TODO: Submit to relay (same as Lesson 6) + + console.log('\n✅ Gasless USDC transaction sent!') + console.log('🔗 View:', `https://polygonscan.com/tx/${txHash}`) + console.log('\n💡 Key differences from USDT:') + console.log(' ✅ EIP-2612 Permit (not meta-transaction)') + console.log(' ✅ Version-based domain separator') + console.log(' ✅ transferWithPermit method (not transferWithApproval)') + console.log(' ✅ Deadline parameter (not functionSignature)') +} + +main().catch((error) => { + console.error('\n❌ Error:', error.message) +}) diff --git a/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_files/package.json b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_files/package.json new file mode 100644 index 0000000..e00cb64 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_files/package.json @@ -0,0 +1,11 @@ +{ + "name": "gasless-usdc-permit", + "type": "module", + "scripts": { + "usdc": "node index.js" + }, + "dependencies": { + "@opengsn/common": "^2.2.6", + "ethers": "^5.7.2" + } +} diff --git a/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_solution/index.js b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_solution/index.js new file mode 100644 index 0000000..7554b67 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_solution/index.js @@ -0,0 +1,378 @@ +import { HttpClient, HttpWrapper } from '@opengsn/common' +import { TypedRequestData } from '@opengsn/common/dist/EIP712/TypedRequestData.js' +import { ethers } from 'ethers' + +// 🔐 Paste your private key from Lesson 1 here! +const PRIVATE_KEY = '0xPASTE_YOUR_PRIVATE_KEY_HERE_FROM_LESSON_1' + +// Mainnet addresses +const POLYGON_RPC_URL = 'https://polygon-rpc.com' +const USDC_ADDRESS = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' +const TRANSFER_CONTRACT_ADDRESS = '0x3157d422cd1be13AC4a7cb00957ed717e648DFf2' +const RELAY_HUB_ADDRESS = '0x6C28AfC105e65782D9Ea6F2cA68df84C9e7d750d' +const RECEIVER_ADDRESS = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184' + +const TRANSFER_AMOUNT_USDC = '0.01' + +// Uniswap V3 addresses for price queries +const UNISWAP_QUOTER = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6' +const WMATIC_ADDRESS = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' +const USDC_WMATIC_POOL = '0xA374094527e1673A86dE625aa59517c5dE346d32' + +// Method selector for transferWithPermit +const METHOD_SELECTOR_TRANSFER_WITH_PERMIT = '0x36efd16f' + +// ABIs +const USDC_ABI = [ + 'function balanceOf(address) view returns (uint256)', + 'function nonces(address) view returns (uint256)', +] + +const TRANSFER_ABI = [ + 'function transferWithPermit(address token, uint256 amount, address target, uint256 fee, uint256 deadline, bytes32 sigR, bytes32 sigS, uint8 sigV)', + 'function getNonce(address) view returns (uint256)', + 'function getRequiredRelayGas(bytes4 methodId) view returns (uint256)', +] + +const RELAY_HUB_ABI = [ + 'event RelayServerRegistered(address indexed relayManager, uint256 baseRelayFee, uint256 pctRelayFee, string relayUrl)', +] + +const UNISWAP_POOL_ABI = [ + 'function fee() external view returns (uint24)', +] + +const UNISWAP_QUOTER_ABI = [ + 'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)', +] + +async function discoverRelays(provider) { + const relayHub = new ethers.Contract(RELAY_HUB_ADDRESS, RELAY_HUB_ABI, provider) + const currentBlock = await provider.getBlockNumber() + const LOOKBACK_BLOCKS = 1600 // ~1 hour (2s per block) + + const events = await relayHub.queryFilter( + relayHub.filters.RelayServerRegistered(), + currentBlock - LOOKBACK_BLOCKS, + currentBlock, + ) + + const relayMap = new Map() + for (const event of events) { + const { relayManager, baseRelayFee, pctRelayFee, relayUrl } = event.args + relayMap.set(relayUrl, { + url: relayUrl, + manager: relayManager, + baseRelayFee, + pctRelayFee: pctRelayFee.toNumber(), + }) + } + + return Array.from(relayMap.values()) +} + +async function validateRelay(relay, provider) { + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 10000) + + const response = await fetch(`${relay.url}/getaddr`, { signal: controller.signal }) + clearTimeout(timeout) + + if (!response.ok) + return null + + const relayInfo = await response.json() + + // Basic validation + if (!relayInfo.version?.startsWith('2.')) + return null + if (relayInfo.networkId !== '137' && relayInfo.chainId !== '137') + return null + if (!relayInfo.ready) + return null + + const workerBalance = await provider.getBalance(relayInfo.relayWorkerAddress) + if (workerBalance.lt(ethers.utils.parseEther('0.01'))) + return null + + // Fee validation + if (relay.pctRelayFee > 70) + return null + if (relay.baseRelayFee.gt(0)) + return null + + return { + ...relay, + relayWorkerAddress: relayInfo.relayWorkerAddress, + minGasPrice: ethers.BigNumber.from(relayInfo.minGasPrice || 0), + version: relayInfo.version, + } + } + catch { + return null + } +} + +async function getPolUsdcPrice(provider) { + // Query Uniswap V3 pool for USDC/WMATIC price + const pool = new ethers.Contract(USDC_WMATIC_POOL, UNISWAP_POOL_ABI, provider) + const quoter = new ethers.Contract(UNISWAP_QUOTER, UNISWAP_QUOTER_ABI, provider) + + // Get pool fee tier + const fee = await pool.fee() + + // Quote: How much POL for 1 USDC (1_000_000 base units)? + const polAmountOut = await quoter.callStatic.quoteExactInputSingle( + USDC_ADDRESS, // tokenIn (USDC) + WMATIC_ADDRESS, // tokenOut (WMATIC) + fee, // pool fee + ethers.utils.parseUnits('1', 6), // 1 USDC + 0, // sqrtPriceLimitX96 + ) + + return polAmountOut // POL wei per 1 USDC +} + +async function calculateOptimalFee(relay, provider, transferContract, isMainnet = true) { + // Step 1: Get network gas price + const networkGasPrice = await provider.getGasPrice() + + console.log(' Network gas price:', ethers.utils.formatUnits(networkGasPrice, 'gwei'), 'gwei') + console.log(' Relay min gas price:', ethers.utils.formatUnits(relay.minGasPrice, 'gwei'), 'gwei') + + // Step 2: Take max of network and relay minimum + const baseGasPrice = networkGasPrice.gt(relay.minGasPrice) ? networkGasPrice : relay.minGasPrice + + // Step 3: Apply buffer (10% mainnet, 25% testnet) + const bufferPercentage = isMainnet ? 110 : 125 + const bufferedGasPrice = baseGasPrice.mul(bufferPercentage).div(100) + + console.log(' Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'gwei'), 'gwei', `(${bufferPercentage}%)`) + + // Step 4: Get gas limit from transfer contract + const gasLimit = await transferContract.getRequiredRelayGas(METHOD_SELECTOR_TRANSFER_WITH_PERMIT) + + console.log(' Gas limit:', gasLimit.toString()) + + // Step 5: Calculate base cost + const baseCost = bufferedGasPrice.mul(gasLimit) + + // Step 6: Apply relay percentage fee + const costWithPctFee = baseCost.mul(100 + relay.pctRelayFee).div(100) + + // Step 7: Add base relay fee + const totalPOLCost = costWithPctFee.add(relay.baseRelayFee) + + console.log(' Total POL cost:', ethers.utils.formatEther(totalPOLCost), 'POL') + console.log(' Relay fee:', `${relay.pctRelayFee}%`) + + // Step 8: Get real-time POL/USDC price from Uniswap + const polPerUsdc = await getPolUsdcPrice(provider) + console.log(' Uniswap rate:', ethers.utils.formatEther(polPerUsdc), 'POL per USDC') + + // Step 9: Convert POL fee to USDC with 10% buffer + // totalPOLCost (POL wei) / polPerUsdc (POL wei per USDC) = USDC base units + const feeInUSDC = totalPOLCost.mul(1_000_000).div(polPerUsdc).mul(110).div(100) + + console.log(' USDC fee:', ethers.utils.formatUnits(feeInUSDC, 6), 'USDC') + + return { + usdcFee: feeInUSDC, + gasPrice: bufferedGasPrice, + gasLimit, + polCost: totalPOLCost, + } +} + +async function findBestRelay(provider, transferContract) { + console.log('\n🔍 Discovering relays...') + const relays = await discoverRelays(provider) + console.log(`Found ${relays.length} unique relay URLs`) + + console.log('\n🔬 Validating and calculating fees...\n') + + let bestRelay = null + let lowestFee = ethers.constants.MaxUint256 + + for (const relay of relays) { + const validRelay = await validateRelay(relay, provider) + + if (!validRelay) + continue + + console.log(`📊 ${relay.url}`) + + try { + const feeData = await calculateOptimalFee(validRelay, provider, transferContract) + + if (feeData.usdcFee.lt(lowestFee)) { + lowestFee = feeData.usdcFee + bestRelay = { ...validRelay, feeData } + console.log(' ✅ New best relay!\n') + } + else { + console.log(' ⚪ Not the cheapest\n') + } + } + catch (error) { + console.log(' ❌ Fee calculation failed:', error.message, '\n') + } + } + + if (!bestRelay) { + throw new Error('No valid relays found') + } + + return bestRelay +} + +async function main() { + console.log('🚀 Gasless USDC transfer with EIP-2612 Permit...\n') + + const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL) + const wallet = new ethers.Wallet(PRIVATE_KEY, provider) + + console.log('🔑 Sender:', wallet.address) + console.log('📍 Receiver:', RECEIVER_ADDRESS) + + // Setup transfer contract + const transferContract = new ethers.Contract(TRANSFER_CONTRACT_ADDRESS, TRANSFER_ABI, provider) + + // Find best relay with optimized fee + const relay = await findBestRelay(provider, transferContract) + + console.log('\n✅ Selected relay:', relay.url) + console.log(' Worker:', relay.relayWorkerAddress) + console.log(' Optimized USDC fee:', ethers.utils.formatUnits(relay.feeData.usdcFee, 6), 'USDC') + console.log(' Gas price:', ethers.utils.formatUnits(relay.feeData.gasPrice, 'gwei'), 'gwei') + + // Build transaction with calculated fee + const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, provider) + const usdcNonce = await usdc.nonces(wallet.address) + + const transferAmount = ethers.utils.parseUnits(TRANSFER_AMOUNT_USDC, 6) + const feeAmount = relay.feeData.usdcFee // Use optimized fee + const approvalAmount = transferAmount.add(feeAmount) + + console.log('\n💰 Transfer:', TRANSFER_AMOUNT_USDC, 'USDC') + console.log('💸 Optimized fee:', ethers.utils.formatUnits(feeAmount, 6), 'USDC') + console.log('✅ Total approval:', ethers.utils.formatUnits(approvalAmount, 6), 'USDC') + + // Sign USDC permit (EIP-2612) + const deadline = ethers.constants.MaxUint256 + + const usdcDomain = { + name: 'USD Coin', + version: '2', + chainId: 137, + verifyingContract: USDC_ADDRESS, + } + + const usdcTypes = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + } + + const usdcMessage = { + owner: wallet.address, + spender: TRANSFER_CONTRACT_ADDRESS, + value: approvalAmount, + nonce: usdcNonce.toNumber(), + deadline, + } + + const approvalSignature = await wallet._signTypedData(usdcDomain, usdcTypes, usdcMessage) + const { r: sigR, s: sigS, v: sigV } = ethers.utils.splitSignature(approvalSignature) + + // Build transfer calldata + const transferCalldata = transferContract.interface.encodeFunctionData('transferWithPermit', [ + USDC_ADDRESS, + transferAmount, + RECEIVER_ADDRESS, + feeAmount, + deadline, + sigR, + sigS, + sigV, + ]) + + // Build relay request with optimized gas price + const forwarderNonce = await transferContract.getNonce(wallet.address) + const currentBlock = await provider.getBlockNumber() + const validUntil = currentBlock + (2 * 60 * 2) // 2 hours + + const relayRequest = { + request: { + from: wallet.address, + to: TRANSFER_CONTRACT_ADDRESS, + value: '0', + gas: relay.feeData.gasLimit.toString(), + nonce: forwarderNonce.toString(), + data: transferCalldata, + validUntil: validUntil.toString(), + }, + relayData: { + gasPrice: relay.feeData.gasPrice.toString(), + pctRelayFee: relay.pctRelayFee.toString(), + baseRelayFee: relay.baseRelayFee.toString(), + relayWorker: relay.relayWorkerAddress, + paymaster: TRANSFER_CONTRACT_ADDRESS, + forwarder: TRANSFER_CONTRACT_ADDRESS, + paymasterData: '0x', + clientId: '1', + }, + } + + // Sign relay request + const forwarderDomain = { + name: 'Forwarder', + version: '1', + chainId: 137, + verifyingContract: TRANSFER_CONTRACT_ADDRESS, + } + + const { types, domain, primaryType, message } = new TypedRequestData( + forwarderDomain.chainId.toString(), + forwarderDomain.verifyingContract, + relayRequest, + ) + + const relaySignature = await wallet._signTypedData(domain, types, message) + + // Submit to relay + console.log('\n📡 Submitting to relay...') + const relayNonce = await provider.getTransactionCount(relay.relayWorkerAddress) + + const httpClient = new HttpClient(new HttpWrapper(), console) + const relayResponse = await httpClient.relayTransaction(relay.url, { + relayRequest, + metadata: { + signature: relaySignature, + approvalData: '0x', + relayHubAddress: RELAY_HUB_ADDRESS, + relayMaxNonce: relayNonce + 3, + }, + }) + + const txHash = typeof relayResponse === 'string' + ? relayResponse + : relayResponse.signedTx || relayResponse.txHash + + console.log('\n✅ Gasless USDC transaction sent!') + console.log('🔗 View:', `https://polygonscan.com/tx/${txHash}`) + console.log('\n💡 Key differences from USDT:') + console.log(' ✅ EIP-2612 Permit (not meta-transaction)') + console.log(' ✅ Version-based domain separator') + console.log(' ✅ transferWithPermit method (not transferWithApproval)') + console.log(' ✅ Deadline parameter (not functionSignature)') +} + +main().catch((error) => { + console.error('\n❌ Error:', error.message) +}) diff --git a/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_solution/package.json b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_solution/package.json new file mode 100644 index 0000000..e00cb64 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/_solution/package.json @@ -0,0 +1,11 @@ +{ + "name": "gasless-usdc-permit", + "type": "module", + "scripts": { + "usdc": "node index.js" + }, + "dependencies": { + "@opengsn/common": "^2.2.6", + "ethers": "^5.7.2" + } +} diff --git a/src/content/tutorial/6-gasless-transfers/7-usdc-permit/content.md b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/content.md new file mode 100644 index 0000000..3c0027d --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/7-usdc-permit/content.md @@ -0,0 +1,288 @@ +--- +type: lesson +title: "USDC with EIP-2612 Permit" +focus: /index.js +mainCommand: npm run usdc +prepareCommands: + - npm install +terminal: + open: true + activePanel: 0 + panels: ['output'] +--- + +# USDC with EIP-2612 Permit + +In Lesson 6 you built gasless USDT transfers using a custom meta-transaction approval. USDC ships with a standardized approval flow called **EIP-2612 Permit**. This lesson explains what that standard changes, why it exists, and how to adapt the gasless pipeline you already wrote so that USDC transfers go through the same relay infrastructure. + +--- + +## Learning Goals + +- Understand when to prefer EIP-2612 Permit instead of custom meta-transaction approvals. +- Sign permit messages that use the version + chainId domain separator defined by EIP-2612. +- Swap the `transferWithApproval` call for the USDC-specific `transferWithPermit`. +- Adjust fee calculations to respect USDC's 6-decimal precision and Polygon USD pricing. + +You already know the broader flow: discover relays, compute fees, sign an approval, relay the transaction. The only moving pieces are the approval signature and the calldata that consumes it. Everything else stays intact. + +--- + +## Background: What EIP-2612 Adds + +EIP-2612 is an extension of ERC-20 that lets a token holder authorize spending via an off-chain signature instead of an on-chain `approve()` transaction. The signature uses the shared EIP-712 typed-data format: + +- **Domain separator** includes the token name, version, chainId, and contract address so signatures cannot be replayed across chains or forks. +- **Permit struct** defines the spender, allowance value, and deadline in a predictable shape. + +Tokens like USDC, DAI, and WETH adopted the standard because it enables wallets and relayers to cover approval gas costs while staying interoperable with any contract that understands permits (for example, Uniswap routers or Aave). + +Older tokens such as USDT predate EIP-2612, so they expose custom meta-transaction logic instead. That is why Lesson 6 had to sign the entire `transferWithApproval` function payload, whereas USDC only needs the numeric values that describe the allowance. + +--- + +## EIP-2612 Permit vs Meta-Transaction + +### USDT Meta-Transaction (Lesson 6) +```js +// Salt-based domain separator +const domain = { + name: 'USDT0', + version: '1', + verifyingContract: USDT_ADDRESS, + salt: ethers.utils.hexZeroPad(ethers.utils.hexlify(137), 32), // ⚠️ Salt, not chainId +} + +const types = { + MetaTransaction: [ + { name: 'nonce', type: 'uint256' }, + { name: 'from', type: 'address' }, + { name: 'functionSignature', type: 'bytes' }, // ⚠️ Encoded function call + ], +} +``` + +### USDC Permit (This Lesson) +```js +// Version-based domain separator (EIP-2612 standard) +const domain = { + name: 'USD Coin', + version: '2', + chainId: 137, // ✅ Standard chainId + verifyingContract: USDC_ADDRESS, +} + +const types = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, // ✅ Deadline, not functionSignature + ], +} +``` + +--- + +## Key Differences + +| Aspect | USDT Meta-Transaction (Lesson 6) | USDC Permit (This Lesson) | +|--------|---------------------------------|---------------------------| +| **Standardization** | Custom, tether-specific | Formalized in EIP-2612 | +| **Domain separator** | Uses `salt` derived from chain | Uses `version` plus `chainId` | +| **Typed struct** | `MetaTransaction` with encoded bytes | `Permit` with discrete fields | +| **Expiry control** | No expiration | Explicit `deadline` | +| **Transfer helper** | `transferWithApproval` | `transferWithPermit` | +| **Method selector** | `0x8d89149b` | `0x36efd16f` | + +Keep this table nearby while refactoring; you will touch each of these rows as you migrate the code. + +--- + +## Step 1: Update Contract Addresses + +```js title="usdc-config.js" showLineNumbers mark=1-5 +const USDC_ADDRESS = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' +const TRANSFER_CONTRACT_ADDRESS = '0x3157d422cd1be13AC4a7cb00957ed717e648DFf2' +const USDC_WMATIC_POOL = '0xA374094527e1673A86dE625aa59517c5dE346d32' + +const METHOD_SELECTOR_TRANSFER_WITH_PERMIT = '0x36efd16f' +``` + +USDC relies on a different relay contract and Uniswap pool than USDT on Polygon. Updating the constants up front prevents subtle bugs later on. For example, querying gas data against the wrong selector yields an optimistic fee that fails on-chain. + +--- + +## Step 2: Sign EIP-2612 Permit + +```js title="permit-signature.js" showLineNumbers mark=1-29 +const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, provider) +const usdcNonce = await usdc.nonces(wallet.address) + +const transferAmount = ethers.utils.parseUnits('0.01', 6) +const feeAmount = relay.feeData.usdcFee +const approvalAmount = transferAmount.add(feeAmount) + +// EIP-2612 domain +const domain = { + name: 'USD Coin', + version: '2', + chainId: 137, + verifyingContract: USDC_ADDRESS, +} + +// EIP-2612 Permit struct +const types = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], +} + +const message = { + owner: wallet.address, + spender: TRANSFER_CONTRACT_ADDRESS, + value: approvalAmount, + nonce: usdcNonce.toNumber(), + deadline: ethers.constants.MaxUint256, // ✅ Infinite deadline +} + +const signature = await wallet._signTypedData(domain, types, message) +const { r, s, v } = ethers.utils.splitSignature(signature) +``` + +Key callouts: + +- Fetch the **nonce** from the USDC contract itself. EIP-2612 uses a per-owner nonce to prevent replay. +- Calculate the **approval value** as `transfer + fee`. A permit is just an allowance, so the relay must be allowed to withdraw both the payment to the recipient and its compensation. +- USDC accepts a `MaxUint256` deadline, but production systems usually set a shorter deadline (for example `Math.floor(Date.now() / 1000) + 3600`) to minimize replay windows. + +--- + +## Step 3: Build transferWithPermit Call + +```js title="transfer-calldata.js" showLineNumbers mark=1-9 +const transferContract = new ethers.Contract( + TRANSFER_CONTRACT_ADDRESS, + TRANSFER_ABI, + provider +) + +const transferCalldata = transferContract.interface.encodeFunctionData('transferWithPermit', [ + USDC_ADDRESS, // token + transferAmount, // amount + RECEIVER_ADDRESS, // target + feeAmount, // fee + deadline, // deadline ⚠️ New parameter + r, // sigR + s, // sigS + v, // sigV +]) +``` + +`transferWithPermit` consumes the permit signature directly. Compare this to the USDT version: instead of passing an encoded `approve()` call, you now hand the relay the raw signature components plus a deadline. + +If you changed the `deadline` value when signing the permit, make sure the same variable is used here. Hardcoding `MaxUint256` in one place and not the other invalidates the signature. + +--- + +## Step 4: Update Fee Calculation + +```js title="usdc-fees.js" showLineNumbers mark=1-11 +// Use USDC-specific method selector +const METHOD_SELECTOR = '0x36efd16f' // transferWithPermit + +const gasLimit = await transferContract.getRequiredRelayGas(METHOD_SELECTOR) + +// Use USDC/WMATIC Uniswap pool +const USDC_WMATIC_POOL = '0xA374094527e1673A86dE625aa59517c5dE346d32' + +const polPerUsdc = await getPolUsdcPrice(provider) // Query USDC pool +const feeInUSDC = totalPOLCost.mul(1_000_000).div(polPerUsdc).mul(110).div(100) +``` + +- `getRequiredRelayGas` evaluates the gas buffer the forwarder demands for `transferWithPermit`. It usually matches the USDT value (~72,000 gas) but querying removes guesswork. +- USDC keeps **6 decimals**, so multiply/divide by `1_000_000` when converting between POL and USDC. Avoid using `ethers.utils.parseUnits(..., 18)` out of habit. +- The `polPerUsdc` helper should target the USDC/WMATIC pool; pricing against USDT would skew the fee at times when the two stablecoins diverge. + +--- + +## Step 5: Rest of Flow Stays the Same + +After building the transfer calldata, the rest is identical to Lesson 6: + +1. Get forwarder nonce from transfer contract +2. Build OpenGSN `ForwardRequest` +3. Sign ForwardRequest with EIP-712 +4. Submit to relay via `HttpClient` +5. Broadcast transaction + +If you already wrapped these steps in helper functions, you should not need to touch them. The permit signature simply slots into the existing request payload where the USDT approval bytes previously sat. + +--- + +## ABI Changes + +```js title="usdc-abi.js" showLineNumbers mark=1-5 +const TRANSFER_ABI = [ + // Changed from transferWithApproval + 'function transferWithPermit(address token, uint256 amount, address target, uint256 fee, uint256 deadline, bytes32 sigR, bytes32 sigS, uint8 sigV)', + 'function getNonce(address) view returns (uint256)', + 'function getRequiredRelayGas(bytes4 methodId) view returns (uint256)', +] +``` + +`transferWithPermit` mirrors OpenZeppelin's relay helper, so the ABI change is straightforward. Keeping the ABI narrowly scoped makes tree-shaking easier if you bundle the tutorial for production later. + +--- + +## Why Two Approval Methods? + +**USDT** (pre-EIP-2612 era): +- Custom meta-transaction implementation +- Salt-based domain separator +- Encodes full function call in signature + +**USDC** (EIP-2612 compliant): +- Standardized permit interface +- Version + chainId domain separator +- Simpler parameter structure + +Most modern tokens (DAI, USDC, WBTC on some chains) support EIP-2612. Older tokens like USDT use custom approaches. + +--- + +## Testing and Troubleshooting + +- **Signature mismatch**: Double-check that `domain.name` exactly matches the on-chain token name. For USDC on Polygon it is `"USD Coin"`; capitalization matters. +- **Invalid deadline**: If the relay says the permit expired, inspect the value you passed to `deadline` and ensure your local clock is not skewed. +- **Allowance too low**: If the recipient receives funds but the relay reverts, print the computed `feeAmount` and make sure the permit covered both transfer and fee. + +Running `npm run usdc` after each change keeps the feedback loop tight and mirrors how the Nimiq wallet tests the same flow. + +--- + +## Production Considerations + +1. **Check token support**: Not all ERC20s have permit. Fallback to standard `approve()` + `transferFrom()` if needed. +2. **Deadline vs MaxUint256**: Production systems often use block-based deadlines (e.g., `currentBlock + 100`) for tighter security. +3. **Domain parameters**: Always verify `name` and `version` match the token contract - wrong values = invalid signature. +4. **Method selector lookup**: Store selectors in config per token to avoid hardcoding. +5. **Permit reuse policy**: Decide whether to reuse a permit for multiple transfers or issue a fresh one per relay request. Fresh permits simplify accounting but require re-signing each time. + +--- + +## Wrap-Up + +You now support gasless transfers for both USDT (custom meta-transaction) and USDC (EIP-2612 permit). Keep these takeaways in mind: + +- ✅ Approval strategies vary across tokens; detect the capability before deciding on the flow. +- ✅ EIP-2612 standardizes the permit format: domain fields and struct definitions must match exactly. +- ✅ `transferWithPermit` lets you drop the bulky encoded function signature and pass raw signature parts instead. +- ✅ Fee and relay logic remain unchanged once the calldata is assembled correctly. + +You now have a complete gasless transaction system matching the Nimiq wallet implementation, ready for production use on Polygon mainnet. diff --git a/src/content/tutorial/6-gasless-transfers/meta.md b/src/content/tutorial/6-gasless-transfers/meta.md new file mode 100644 index 0000000..0535628 --- /dev/null +++ b/src/content/tutorial/6-gasless-transfers/meta.md @@ -0,0 +1,12 @@ +--- +type: part +title: Gasless Transfers +previews: false +prepareCommands: + - npm install +mainCommand: npm run send +terminal: + open: true + activePanel: 0 + panels: ['output'] +---