1(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,62890,8315,84435,e=>{"use strict";var t=e.i(99873),n=e.i(65600),a=e.i(15300);let s=e=>"object"==typeof e&&null!=e&&1===e.nodeType,i=(e,t)=>(!t||"hidden"!==e)&&"visible"!==e&&"clip"!==e,r=(e,t)=>{if(e.clientHeight<e.scrollHeight||e.clientWidth<e.scrollWidth){let n,a=getComputedStyle(e,null);return i(a.overflowY,t)||i(a.overflowX,t)||!!(n=(e=>{if(!e.ownerDocument||!e.ownerDocument.defaultView)return null;try{return e.ownerDocument.defaultView.frameElement}catch(e){return null}})(e))&&(n.clientHeight<e.scrollHeight||n.clientWidth<e.scrollWidth)}return!1},l=(e,t,n,a,s,i,r,l)=>i<e&&r>t||i>e&&r<t?0:i<=e&&l<=n||r>=t&&l>=n?i-e-a:r>t&&l<n||i<e&&l>n?r-t+s:0,o=e=>{let t=e.parentElement;return null==t?e.getRootNode().host||null:t},c=(e,t)=>{var n,a,i,c;let d;if("u"<typeof document)return[];let{scrollMode:u,block:h,inline:p,boundary:f,skipOverflowHiddenElements:m}=t,x="function"==typeof f?f:e=>e!==f;if(!s(e))throw TypeError("Invalid target");let b=document.scrollingElement||document.documentElement,j=[],y=e;for(;s(y)&&x(y);){if((y=o(y))===b){j.push(y);break}null!=y&&y===document.body&&r(y)&&!r(document.documentElement)||null!=y&&r(y,m)&&j.push(y)}let g=null!=(a=null==(n=window.visualViewport)?void 0:n.width)?a:innerWidth,v=null!=(c=null==(i=window.visualViewport)?void 0:i.height)?c:innerHeight,{scrollX:w,scrollY:N}=window,{height:C,width:S,top:k,right:T,bottom:R,left:P}=e.getBoundingClientRect(),{top:B,right:_,bottom:F,left:I}={top:parseFloat((d=window.getComputedStyle(e)).scrollMarginTop)||0,right:parseFloat(d.scrollMarginRight)||0,bottom:parseFloat(d.scrollMarginBottom)||0,left:parseFloat(d.scrollMarginLeft)||0},$="start"===h||"nearest"===h?k-B:"end"===h?R+F:k+C/2-B+F,A="center"===p?P+S/2-I+_:"end"===p?T+_:P-I,E=[];for(let e=0;e<j.length;e++){let t=j[e],{height:n,width:a,top:s,right:i,bottom:o,left:c}=t.getBoundingClientRect();if("if-needed"===u&&k>=0&&P>=0&&R<=v&&T<=g&&(t===b&&!r(t)||k>=s&&R<=o&&P>=c&&T<=i))break;let d=getComputedStyle(t),f=parseInt(d.borderLeftWidth,10),m=parseInt(d.borderTopWidth,10),x=parseInt(d.borderRightWidth,10),y=parseInt(d.borderBottomWidth,10),B=0,_=0,F="offsetWidth"in t?t.offsetWidth-t.clientWidth-f-x:0,I="offsetHeight"in t?t.offsetHeight-t.clientHeight-m-y:0,O="offsetWidth"in t?0===t.offsetWidth?0:a/t.offsetWidth:0,K="offsetHeight"in t?0===t.offsetHeight?0:n/t.offsetHeight:0;if(b===t)B="start"===h?$:"end"===h?$-v:"nearest"===h?l(N,N+v,v,m,y,N+$,N+$+C,C):$-v/2,_="start"===p?A:"center"===p?A-g/2:"end"===p?A-g:l(w,w+g,g,f,x,w+A,w+A+S,S),B=Math.max(0,B+N),_=Math.max(0,_+w);else{B="start"===h?$-s-m:"end"===h?$-o+y+I:"nearest"===h?l(s,o,n,m,y+I,$,$+C,C):$-(s+n/2)+I/2,_="start"===p?A-c-f:"center"===p?A-(c+a/2)+F/2:"end"===p?A-i+x+F:l(c,i,a,f,x+F,A,A+S,S);let{scrollLeft:e,scrollTop:r}=t;B=0===K?0:Math.max(0,Math.min(r+B/K,t.scrollHeight-n/K+I)),_=0===O?0:Math.max(0,Math.min(e+_/O,t.scrollWidth-a/O+F)),$+=r-B,A+=e-_}E.push({el:t,top:B,left:_})}return E};var d=(0,a.createContext)([]),u=(0,a.createContext)({current:null});function h(){return(0,a.useContext)(d)}function p({containerRef:e,children:n}){return(0,t.jsx)(u.Provider,{value:e,children:n})}function f({toc:e,single:n=!0,children:s}){let i=(0,a.useMemo)(()=>e.map(e=>e.url.split("#")[1]),[e]);return(0,t.jsx)(d.Provider,{value:function(e,t){let[n,s]=(0,a.useState)([]);return(0,a.useEffect)(()=>{let n=[],a=new IntersectionObserver(e=>{for(let t of e)t.isIntersecting&&!n.includes(t.target.id)?n=[...n,t.target.id]:!t.isIntersecting&&n.includes(t.target.id)&&(n=n.filter(e=>e!==t.target.id));n.length>0&&s(n)},{rootMargin:t?"-80px 0% -70% 0%":"-20px 0% -40% 0%",threshold:1});function i(){let n=document.scrollingElement;if(!n)return;let a=n.scrollTop;a<=0&&t?s(e.slice(0,1)):a+n.clientHeight>=n.scrollHeight-6&&s(n=>n.length>0&&!t?e.slice(e.indexOf(n[0])):e.slice(-1))}for(let t of e){let e=document.getElementById(t);e&&a.observe(e)}return i(),window.addEventListener("scroll",i),()=>{window.removeEventListener("scroll",i),a.disconnect()}},[t,e]),t?n.slice(0,1):n}(i,n),children:s})}var m=(0,a.forwardRef)(({onActiveChange:e,...s},i)=>{let r=(0,a.useContext)(u),l=h(),o=(0,a.useRef)(null),d=function(...e){return t=>
1{e.forEach(e=>{"function"==typeof e?e(t):null!==e&&(e.current=t)})}}(o,i),p=l.includes(s.href.slice(1));return(0,n.useOnChange)(p,t=>{let n=o.current;n&&(t&&r.current&&function(e,t){let n;if(!e.isConnected||!(e=>{let t=e;for(;t&&t.parentNode;){if(t.parentNode===document)return!0;t=t.parentNode instanceof ShadowRoot?t.parentNode.host:t.parentNode}return!1})(e))return;let a={top:parseFloat((n=window.getComputedStyle(e)).scrollMarginTop)||0,right:parseFloat(n.scrollMarginRight)||0,bottom:parseFloat(n.scrollMarginBottom)||0,left:parseFloat(n.scrollMarginLeft)||0};if("object"==typeof t&&"function"==typeof t.behavior)return t.behavior(c(e,t));let s="boolean"==typeof t||null==t?void 0:t.behavior;for(let{el:n,top:i,left:r}of c(e,!1===t?{block:"end",inline:"nearest"}:t===Object(t)&&0!==Object.keys(t).length?t:{block:"start",inline:"nearest"})){let e=i-a.top+a.bottom,t=r-a.left+a.right;n.scroll({top:e,left:t,behavior:s})}}(n,{behavior:"smooth",block:"center",inline:"center",scrollMode:"always",boundary:r.current}),e?.(t))}),(0,t.jsx)("a",{ref:d,"data-active":p,...s,children:s.children})});m.displayName="TOCItem",e.s(["AnchorProvider",0,f,"ScrollProvider",0,p,"TOCItem",0,m,"useActiveAnchor",0,function(){return(0,a.useContext)(d).at(-1)},"useActiveAnchors",0,h],8315);var x=e.i(83137),b=e.i(86901);e.i(28519);var j=e.i(59806);function y(e,t){if(0===t.length||0===e.clientHeight)return[0,0];let n=Number.MAX_VALUE,a=0;for(let s of t){let t=e.querySelector(`a[href="#${s}"]`);if(!t)continue;let i=getComputedStyle(t);n=Math.min(n,t.offsetTop+parseFloat(i.paddingTop)),a=Math.max(a,t.offsetTop+t.clientHeight-parseFloat(i.paddingBottom))}return[n,a-n]}function g(e,t){e.style.setProperty("--fd-top",`${t[0]}px`),e.style.setProperty("--fd-height",`${t[1]}px`)}function v({containerRef:e,...s}){let i=h(),r=(0,a.useRef)(null),l=(0,j.useEffectEvent)(()=>{e.current&&r.current&&g(r.current,y(e.current,i))});return(0,a.useEffect)(()=>{if(!e.current)return;let t=e.current;l();let n=new ResizeObserver(l);return n.observe(t),()=>{n.disconnect()}},[e,l]),(0,n.useOnChange)(i,()=>{e.current&&r.current&&g(r.current,y(e.current,i))}),(0,t.jsx)("div",{ref:r,role:"none",...s})}e.s(["TocThumb",0,v],84435);var w=e.i(10745);let N=(0,a.createContext)([]);function C(){return(0,a.useContext)(N)}function S({item:e}){return(0,t.jsx)(m,{href:e.url,className:(0,x.cn)("prose py-1.5 text-sm text-fd-muted-foreground transition-colors [overflow-wrap:anywhere] first:pt-0 last:pb-0 data-[active=true]:text-fd-primary",e.depth<=2&&"ps-3",3===e.depth&&"ps-6",e.depth>=4&&"ps-8"),children:e.title})}e.s(["TOCItems",0,function({ref:e,className:n,...s}){let i=(0,a.useRef)(null),r=C(),{text:l}=(0,b.useI18n)();return 0===r.length?(0,t.jsx)("div",{className:"rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground",children:l.tocNoHeadings}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(v,{containerRef:i,className:"absolute top-(--fd-top) h-(--fd-height) w-px bg-fd-primary transition-all"}),(0,t.jsx)("div",{ref:(0,w.mergeRefs)(e,i),className:(0,x.cn)("flex flex-col border-s border-fd-foreground/10",n),...s,children:r.map(e=>(0,t.jsx)(S,{item:e},e.url))})]})},"TOCProvider",0,function({toc:e,children:n,...a}){return(0,t.jsx)(N,{value:e,children:(0,t.jsx)(f,{toc:e,...a,children:n})})},"TOCScrollArea",0,function({ref:e,className:n,...s}){let i=(0,a.useRef)(null);return(0,t.jsx)("div",{ref:(0,w.mergeRefs)(i,e),className:(0,x.cn)("relative min-h-0 text-sm ms-px overflow-auto [scrollbar-width:none] [mask-image:linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3",n),...s,children:(0,t.jsx)(p,{containerRef:i,children:s.children})})},"useTOCItems",0,C],62890)},42986,e=>{"use strict";var t=e.i(99873),n=e.i(15300),a=e.i(927);e.i(61980);var s=e.i(73936),i=e.i(83137),r=e.i(86901),l=e.i(10358);e.i(17097);var o=e.i(8142),c=e.i(66653),d=e.i(65264),u=e.i(74353),h=e.i(59806),p=e.i(30790),f=e.i(8880),m=e.i(62890),x=e.i(8315);let b=(0,o.createContext)("TocPopoverContext");function j({value:e,strokeWidth:n=2,size:a=24,min:s=0,max:i=100,...r}){let l=e<s?s:e>i?i:e,o=(a-n)/2,c=2*Math.PI*o,d=l/i*c,u={cx:a/2,cy:a/2,r:o,fill:"none",strokeWidth:n};return(0,t.jsxs)("svg",{role:"progressbar",viewBox:`0 0 ${a} ${a}`,"aria-valuenow":l,"aria-valuemin":s,"aria-valuemax":i,...r,children:[(0,t.jsx)("circle",{...u,className:"stroke-current/25"}),(0,t.jsx)("circle",{...u,stroke:"currentColor",strokeDasharray:c,strokeDashoffset:c-d,strokeLinecap:"round",transform:`rotate(-90 ${a/2} ${a/2})`,className:"transition-all"})]})}let y=new WeakMap;function g({item:e,index:n}){let{text:l}=(0,r.useI18n)(),o=0===n?a.ChevronLeft:a.ChevronRight;return(0,t.jsxs)(s.default,{href:e.url,className:(0,i.cn)("flex flex-col gap-2 rounded-lg border p-4 text-sm transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground @max-lg:col-span-full",1===n&&"text-end"),children:[(0,t.jsxs)("div",{className:(0,i.cn)("inline-flex items-center gap-1.5 font-medium",1===n&&"flex-row-reverse"),children:[(0,t.jsx)(o,{className:"-mx-1 size-4 shrink-0 rtl:rotate-180"}),(0,t.jsx)("p",{children:e.name})]}),(0,t.jsx)("p",{className:"text-fd-muted-foreground truncate",children:e.description??(0===n?l.previousPage:l.nextPage)})]})}e.s(["PageBreadcrumb",0,function({includeRoot:e=!1,includeSeparator:a,includePage:r=!1,...o}){let d=(0,l.useTreePath)(),{root:u}=(0,l.useTreeContext)(),h=(0,n.useMemo)(()=>(0,c.getBreadcrumbItemsFromPath)(u,d,{includePage:r,includeSeparator:a,includeRoot:e}),[r,e,a,d,u]);return 0===h.length?null:(0,t.jsx)("div",{...o,className:(0,i.cn)("flex items-center gap-1.5 text-[15px] text-fd-muted-foreground",o.className),children:h.map((e,a)=>
1{let r=(0,i.cn)("truncate",a===h.length-1&&"text-fd-primary font-medium");return(0,t.jsxs)(n.Fragment,{children:[0!==a&&(0,t.jsx)("span",{className:"text-fd-foreground/30",children:"/"}),e.url?(0,t.jsx)(s.default,{href:e.url,className:(0,i.cn)(r,"transition-opacity hover:opacity-80"),children:e.name}):(0,t.jsx)("span",{className:r,children:e.name})]},a)})})},"PageFooter",0,function({items:e,...a}){let{root:s}=(0,l.useTreeContext)(),r=(0,o.usePathname)(),{previous:c,next:d}=(0,n.useMemo)(()=>{if(e)return e;let t=y.get(s)??function e(t){let n=[];return t.forEach(t=>{if("folder"===t.type){t.index&&n.push(t.index),n.push(...e(t.children));return}"page"!==t.type||t.external||n.push(t)}),n}(s.children);y.set(s,t);let n=t.findIndex(e=>(0,u.isActive)(e.url,r,!1));return -1===n?{}:{previous:t[n-1],next:t[n+1]}},[e,r,s]);return(0,t.jsxs)("div",{...a,className:(0,i.cn)("@container grid gap-4 pb-6",c&&d?"grid-cols-2":"grid-cols-1",a.className),children:[c?(0,t.jsx)(g,{item:c,index:0}):null,d?(0,t.jsx)(g,{item:d,index:1}):null]})},"PageLastUpdate",0,function({date:e,...a}){let{text:s}=(0,r.useI18n)(),[l,o]=(0,n.useState)("");return(0,n.useEffect)(()=>{o(new Date(e).toLocaleDateString())},[e]),(0,t.jsxs)("p",{...a,className:(0,i.cn)("text-sm text-fd-muted-foreground",a.className),children:[s.lastUpdate," ",l]})},"PageRoot",0,function({toc:e,children:n,...a}){let{collapsed:s}=(0,f.useSidebar)();return(0,t.jsx)(m.TOCProvider,{...e,children:(0,t.jsx)("div",{id:"nd-page",...a,className:(0,i.cn)("flex flex-1 mx-auto w-full",a.className),style:{paddingTop:"calc(var(--fd-nav-height) + var(--fd-tocnav-height))",maxWidth:s?"var(--fd-page-width)":"min(var(--fd-page-width),calc(var(--fd-layout-width) - var(--fd-sidebar-width)))",...a.style},children:n})})},"PageTOC",0,function(e){return(0,t.jsx)("div",{id:"nd-toc",...e,className:(0,i.cn)("sticky pb-2 pt-12 max-xl:hidden",e.className),style:{...e.style,top:"calc(var(--fd-banner-height) + var(--fd-nav-height))",height:"calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))"},children:(0,t.jsx)("div",{className:"flex h-full w-(--fd-toc-width) max-w-full flex-col pe-4",children:e.children})})},"PageTOCPopover",0,function(e){let a=(0,n.useRef)(null),[s,r]=(0,n.useState)(!1),{collapsed:l}=(0,f.useSidebar)(),{isTransparent:o}=(0,d.useNav)(),c=(0,h.useEffectEvent)(e=>{s&&a.current&&!a.current.contains(e.target)&&r(!1)});return(0,n.useEffect)(()=>(window.addEventListener("click",c),()=>{window.removeEventListener("click",c)}),[c]),(0,t.jsx)(b.Provider,{value:(0,n.useMemo)(()=>({open:s,setOpen:r}),[r,s]),children:(0,t.jsx)(p.Collapsible,{open:s,onOpenChange:r,asChild:!0,children:(0,t.jsx)("header",{ref:a,id:"nd-tocnav",...e,className:(0,i.cn)("fixed inset-x-0 z-10 border-b backdrop-blur-sm transition-colors xl:hidden",(!o||s)&&"bg-fd-background/80",s&&"shadow-lg",e.className),style:{...e.style,top:"calc(var(--fd-banner-height) + var(--fd-nav-height))",insetInlineStart:l?"0px":"calc(var(--fd-sidebar-width) + var(--fd-layout-offset))"},children:e.children})})})},"PageTOCPopoverContent",0,function(e){return(0,t.jsx)(p.CollapsibleContent,{"data-toc-popover":"",...e,className:(0,i.cn)("flex flex-col px-4 max-h-[50vh] md:px-6",e.className),children:e.children})},"PageTOCPopoverTrigger",0,function(e){let{text:s}=(0,r.useI18n)(),{open:o}=b.use(),c=(0,m.useTOCItems)(),d=(0,x.useActiveAnchor)(),u=(0,n.useMemo)(()=>c.findIndex(e=>d===e.url.slice(1)),[c,d]),h=(0,l.useTreePath)().at(-1),f=-1!==u&&!o;return(0,t.jsxs)(p.CollapsibleTrigger,{...e,className:(0,i.cn)("flex w-full h-(--fd-tocnav-height) items-center text-sm text-fd-muted-foreground gap-2.5 px-4 py-2.5 text-start focus-visible:outline-none [&_svg]:shrink-0 [&_svg]:size-4 md:px-6",e.className),children:[(0,t.jsx)(j,{value:(u+1)/Math.max(1,c.length),max:1,className:(0,i.cn)(o&&"text-fd-primary")}),(0,t.jsxs)("span",{className:"grid flex-1 *:my-auto *:row-start-1 *:col-start-1",children:[(0,t.jsx)("span",{className:(0,i.cn)("truncate transition-all",o&&"text-fd-foreground",f&&"opacity-0 -translate-y-full pointer-events-none"),children:h?.name??s.toc}),(0,t.jsx)("span",{className:(0,i.cn)("truncate transition-all",!f&&"opacity-0 translate-y-full pointer-events-none"),children:c[u]?.title})]}),(0,t.jsx)(a.ChevronDown,{className:(0,i.cn)("transition-transform mx-0.5",o&&"rotate-180")}
1)]})}])},78052,e=>{"use strict";var t=e.i(99873),n=e.i(8315),a=e.i(15300),s=e.i(83137),i=e.i(84435),r=e.i(62890),l=e.i(10745),o=e.i(86901);function c(e){return 10*(e>=3)}function d({item:e,upper:a=e.depth,lower:i=e.depth}){var r;let l=c(e.depth),o=c(a),u=c(i);return(0,t.jsxs)(n.TOCItem,{href:e.url,style:{paddingInlineStart:(r=e.depth)<=2?14:3===r?26:36},className:"prose relative py-1.5 text-sm text-fd-muted-foreground transition-colors [overflow-wrap:anywhere] first:pt-0 last:pb-0 data-[active=true]:text-fd-primary",children:[l!==o?(0,t.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 16 16",className:"absolute -top-1.5 start-0 size-4 rtl:-scale-x-100",children:(0,t.jsx)("line",{x1:o,y1:"0",x2:l,y2:"12",className:"stroke-fd-foreground/10",strokeWidth:"1"})}):null,(0,t.jsx)("div",{className:(0,s.cn)("absolute inset-y-0 w-px bg-fd-foreground/10",l!==o&&"top-1.5",l!==u&&"bottom-1.5"),style:{insetInlineStart:l}}),e.title]})}e.s(["default",0,function({ref:e,className:n,...u}){let h=(0,a.useRef)(null),p=(0,r.useTOCItems)(),{text:f}=(0,o.useI18n)(),[m,x]=(0,a.useState)();return((0,a.useEffect)(()=>{if(!h.current)return;let e=h.current;function t(){if(0===e.clientHeight)return;let t=0,n=0,a=[];for(let s=0;s<p.length;s++){let i=e.querySelector(`a[href="#${p[s].url.slice(1)}"]`);if(!i)continue;let r=getComputedStyle(i),l=c(p[s].depth)+1,o=i.offsetTop+parseFloat(r.paddingTop),d=i.offsetTop+i.clientHeight-parseFloat(r.paddingBottom);t=Math.max(l,t),n=Math.max(n,d),a.push(`${0===s?"M":"L"}${l} ${o}`),a.push(`L${l} ${d}`)}x({path:a.join(" "),width:t+1,height:n})}let n=new ResizeObserver(t);return t(),n.observe(e),()=>{n.disconnect()}},[p]),0===p.length)?(0,t.jsx)("div",{className:"rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground",children:f.tocNoHeadings}):(0,t.jsxs)(t.Fragment,{children:[m?(0,t.jsx)("div",{className:"absolute start-0 top-0 rtl:-scale-x-100",style:{width:m.width,height:m.height,maskImage:`url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${m.width} ${m.height}"><path d="${m.path}" stroke="black" stroke-width="1" fill="none" /></svg>`)}")`},children:(0,t.jsx)(i.TocThumb,{containerRef:h,className:"mt-(--fd-top) h-(--fd-height) bg-fd-primary transition-all"})}):null,(0,t.jsx)("div",{ref:(0,l.mergeRefs)(h,e),className:(0,s.cn)("flex flex-col",n),...u,children:p.map((e,n)=>(0,t.jsx)(d,{item:e,upper:p[n-1]?.depth,lower:p[n+1]?.depth},e.url))})]})}])},68046,e=>{"use strict";var t=e.i(99873),n=e.i(15300);e.s(["Tooltip",0,function({content:e,children:a,position:s="top",className:i=""}){let[r,l]=(0,n.useState)(!1),[o,c]=(0,n.useState)({x:0,y:0}),d=(0,n.useRef)(null),u=(0,n.useRef)(null),h=()=>{if(d.current&&u.current){let e=d.current.getBoundingClientRect(),t=u.current.getBoundingClientRect(),n=window.scrollX,a=window.scrollY,i=0,r=0;switch(s){case"top":i=e.left+n+(e.width-t.width)/2,r=e.top+a-t.height-8;break;case"bottom":i=e.left+n+(e.width-t.width)/2,r=e.bottom+a+8;break;case"left":i=e.left+n-t.width-8,r=e.top+a+(e.height-t.height)/2;break;case"right":i=e.right+n+8,r=e.top+a+(e.height-t.height)/2}c({x:i=Math.max(8,Math.min(i,window.innerWidth-t.width-8)),y:r=Math.max(8,Math.min(r,window.innerHeight-t.height-8))})}};return(0,n.useEffect)(()=>{if(r)return h(),window.addEventListener("scroll",h),window.addEventListener("resize",h),()=>{window.removeEventListener("scroll",h),window.removeEventListener("resize",h)}},[r,s]),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("div",{ref:d,onMouseEnter:()=>l(!0),onMouseLeave:()=>l(!1),className:`cursor-help ${i}`,children:a}),r&&(0,t.jsxs)("div",{ref:u,className:"fixed z-50 px-3 py-2 text-xs font-medium text-white bg-gray-900 dark:bg-gray-100 dark:text-gray-900 rounded-lg shadow-lg border border-gray-700 dark:border-gray-300 max-w-xs opacity-0 animate-fadeIn",style:{left:o.x,top:o.y},children:[(0,t.jsx)("div",{className:(()=>{let e="absolute w-2 h-2 bg-gray-900 dark:bg-gray-100 transform rotate-45";switch(s){case"top":default:return`${e} -bottom-1 left-1/2 -translate-x-1/2`;case"bottom":return`${e} -top-1 left-1/2 -translate-x-1/2`;case"left":return`${e} -right-1 top-1/2 -translate-y-1/2`;case"right":return`${e} -left-1 top-1/2 -translate-y-1/2`}})()}),e]})]})}])},9471,e=>{"use strict";var t=e.i(99873),n=e.i(56615),a=e.i(21239),s=e.i(41178),i=e.i(15300),r=e.i(42829),l=e.i(84678);let o="/dns4/bottle.fiber.channel/tcp/443/wss/p2p/QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo",c="0x02b6d4e3ab86
1a2ca2fad6fae0ecb2e1e559e0b911939872a90abdda6d20302be71",d="fiber-docs:wasm-quickstart-profile-v1",u="fiber-docs:wasm-isolation-reload-v1",h="npm install @fiber-pay/sdk",p=[{title:"Connect to Fiber with a WASM Node",shortTitle:"Connect a WASM node",href:"/docs/build/connect-wasm-node"},{title:"Open a Fiber Channel and Send a Payment",shortTitle:"Open a channel and send a payment",href:"/docs/build/open-channel-payment"},{title:"Send a Multi-Hop Invoice Payment",shortTitle:"Send a multi-hop invoice payment",href:"/docs/build/multi-hop-invoice"},{title:"Open a Unidirectional Fiber Channel",shortTitle:"Open a unidirectional channel",href:"/docs/build/unidirectional-channel"},{title:"Build a Conditional Payment with a Hold Invoice",shortTitle:"Build a Hold Invoice payment",href:"/docs/build/hold-invoice"},{title:"Unlock Encrypted Data with a Fiber Payment",shortTitle:"Unlock encrypted data with a payment",href:"/docs/build/encrypted-data-payment"},{title:"Pay for a Verified Result",shortTitle:"Pay for a verified result",href:"/docs/build/verified-result-payment"},{title:"Pay with RUSD over Fiber",shortTitle:"Pay with RUSD",href:"/docs/build/rusd-payment"},{title:"Close a Fiber Channel and Recover Funds",shortTitle:"Close a channel and recover funds",href:"/docs/build/close-channel"}],f=p.findIndex(e=>"/docs/build/connect-wasm-node"===e.href),m=p[f-1]??{title:"Interactive Tutorials",shortTitle:"Interactive tutorials",href:"/docs/build/interactive-tutorials"},x=p[f+1],b=[{id:"headers",label:"next.config.mjs",language:"javascript",code:`/** @type {import('next').NextConfig} */ 2const nextConfig = { 3 transpilePackages: [ 4 '@fiber-pay/sdk', 5 '@nervosnetwork/fiber-js', 6 ], 7 async headers() { 8 return [{ 9 source: '/fiber', 10 headers: [ 11 { 12 key: 'Cross-Origin-Opener-Policy', 13 value: 'same-origin', 14 }, 15 { 16 key: 'Cross-Origin-Embedder-Policy', 17 value: 'require-corp', 18 }, 19 ], 20 }]; 21 }, 22}; 23 24export default nextConfig;`},{id:"fiber",label:"lib/fiber.ts",language:"typescript",code:`import type { 25 BrowserNodeState, 26 FiberBrowserNode, 27} from '@fiber-pay/sdk/browser'; 28 29export const router = 30 '/dns4/bottle.fiber.channel/tcp/443/wss/p2p/' + 31 'QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo'; 32 33export const routerPubkey = 34 '0x02b6d4e3ab86a2ca2fad6fae0ecb2e1e559e0b911939872a90abdda6d20302be71'; 35 36function randomKey() { 37 return crypto.getRandomValues(new Uint8Array(32)); 38} 39 40export async function startFiber( 41 onState: (state: BrowserNodeState) => void, 42): Promise<FiberBrowserNode> { 43 if (!crossOriginIsolated) { 44 throw new Error('This page must be cross-origin isolated.'); 45 } 46 47 const { 48 FiberBrowserNode, 49 RawKeyCredentialProvider, 50 } = await import('@fiber-pay/sdk/browser'); 51 52 // Testnet quickstart: persist these keys in encrypted 53 // application storage before using this in production. 54 const credential = new RawKeyCredentialProvider( 55 randomKey(), 56 randomKey(), 57 crypto.randomUUID(), 58 ); 59 60 const node = new FiberBrowserNode({ 61 network: 'testnet', 62 credential, 63 nodeConfig: { 64 bootnodes: [], 65 logLevel: 'info', 66 }, 67 }); 68 69 node.on('stateChange', onState); 70 await node.start(); 71 return node; 72} 73 74export async function connectToRouter(node: FiberBrowserNode) { 75 await node.connectPeer({ 76 address: router, 77 pubkey: routerPubkey, 78 }); 79}`},{id:"app",label:"app/fiber/page.tsx",language:"tsx",code:`'use client'; 80 81import { useRef, useState } from 'react'; 82import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 83import { connectToRouter, startFiber } from '@/lib/fiber'; 84 85export default function FiberPage() { 86 const node = useRef<FiberBrowserNode | null>(null); 87 const [status, setStatus] = useState('idle'); 88 const [pubkey, setPubkey] = useState(''); 89 const [peers, setPeers] = useState(0); 90 91 async function start() { 92 const fiber = await startFiber(setStatus); 93 node.current = fiber; 94 const info = await fiber.getNodeInfo(); 95 setPubkey(info.pubkey); 96 } 97 98 async function connect() { 99 if (!node.current) return; 100 await connectToRouter(node.current); 101 const peerList = await node.current.listPeers(); 102 setPeers(peerList.peers.length); 103 } 104 105 return ( 106 <main> 107 <p>Fiber WASM \xb7 {status}</p> 108 <h1>Your Fiber node, inside React.</h1> 109 <button onClick={start}>Start node</button> 110 <button disabled={!node.current} onClick={connect}> 111 Connect peer 112 </button> 113 {pubkey && <code>{pubkey}</code>} 114 <p>{peers} connected peers</p> 115 </main> 116 ); 117}`}],j={isolate:{file:"headers",start:1,end:24},start:{file:"fiber",start:17,end:49},"connect-helper":{file:"fiber",start:51,end:56},"react-store":{file:"app",start:7,end:11},"react-start":{file:"app",start:13,end:18},"react-connect":{file:"app",start:20,end:25},"react-render":{file:"app",start:27,end:39}},y=/(\/\/.*$|\/\*.*?\*\/|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`|<\/?[A-Za-z][\w.]*|\b(?:import|from|export|default|const|let|var|function|async|await|return|if|else|throw|new|typeof|instanceof|true|false|null|undefined|type|interface|extends|implements|as|in|of)\b|\b(?:[A-Z][A-Za-z0-9_]*|[a-zA-Z_$][\w$]*(?=\(
117))\b|\b\d[\d_]*(?:\.\d+)?n?\b)/g;function g(e){return Array.from(e,e=>e.toString(16).padStart(2,"0")).join("")}function v(e){return Uint8Array.from(e.match(/.{2}/g)??[],e=>Number.parseInt(e,16))}function w(e){return e?`${e.slice(0,12)}â¦${e.slice(-8)}`:"Waiting for node"}function N(e){return"running"===e?"Node running":"starting"===e?"Starting WASM":"stopping"===e?"Stopping":"error"===e?"Connection issue":"Ready to start"}function C({file:e,focus:n}){let a=(0,i.useRef)(null);return(0,i.useEffect)(()=>{if(!n)return;let e=a.current,t=e?.querySelector(`[data-line="${n.start}"]`);if(!e||!t)return;let s=t.getBoundingClientRect().top-e.getBoundingClientRect().top+e.scrollTop;e.scrollTo({top:Math.max(0,s-.28*e.clientHeight),behavior:"smooth"})},[e.id,n]),(0,t.jsx)("pre",{className:l.default.code,"aria-label":`${e.label} code`,ref:a,children:(0,t.jsx)("code",{children:e.code.split("\n").map((a,s)=>{let i=s+1,r=n?.file===e.id&&i>=n.start&&i<=n.end;return(0,t.jsxs)("span",{className:`${l.default.codeLine} ${r?l.default.focusedCodeLine:""}`,"data-line":i,children:[(0,t.jsx)("span",{className:l.default.lineNumber,children:i}),(0,t.jsx)("span",{children:a?function(e){let n=[],a=0;for(let i of e.matchAll(y)){var s;let r=i.index??0;r>a&&n.push(e.slice(a,r)),n.push((0,t.jsx)("span",{className:(s=i[0]).startsWith("//")||s.startsWith("/*")?l.default.tokenComment:/^['"`]/.test(s)?l.default.tokenString:s.startsWith("<")?l.default.tokenTag:/^\d/.test(s)?l.default.tokenNumber:/^[A-Z]/.test(s)?l.default.tokenType:/^(true|false|null|undefined)$/.test(s)?l.default.tokenConstant:/^(import|from|export|default|const|let|var|function|async|await|return|if|else|throw|new|typeof|instanceof|type|interface|extends|implements|as|in|of)$/.test(s)?l.default.tokenKeyword:l.default.tokenFunction,children:i[0]},`${r}-${i[0]}`)),a=r+i[0].length}return a<e.length&&n.push(e.slice(a)),n}(a):" "})]},`${e.id}-${s}`)})})})}e.s(["FiberWasmQuickstart",0,function(){let y=(0,s.useRouter)(),S=(0,i.useRef)(null),k=(0,i.useRef)(null),T=(0,i.useRef)(null),R=(0,i.useRef)(null),P=(0,i.useRef)(null),[B,_]=(0,i.useState)("architecture"),[F,I]=(0,i.useState)(0),[$,A]=(0,i.useState)(!1),[E,O]=(0,i.useState)(!1),[K,D]=(0,i.useState)(!1),[L,H]=(0,i.useState)("app"),[W,M]=(0,i.useState)(null),[U,q]=(0,i.useState)("idle"),[z,X]=(0,i.useState)(null),[G,V]=(0,i.useState)(0),[J,Y]=(0,i.useState)(null),[Q,Z]=(0,i.useState)(""),[ee,et]=(0,i.useState)(!1),[en,ea]=(0,i.useState)(!1),[es,ei]=(0,i.useState)(null),er=b.find(e=>e.id===L)??b[0],el=Q?l.default.statusError:"running"===U?l.default.statusSuccess:l.default.statusWaiting,eo=Q?l.default.statusError:G>0?l.default.statusSuccess:l.default.statusWaiting,ec=(0,i.useCallback)(e=>{_(e);let t=j[e];t?(H(t.file),M(t)):M(null)},[]);(0,i.useEffect)(()=>{let e=window.crossOriginIsolated&&"u">typeof SharedArrayBuffer;if(e)window.sessionStorage.removeItem(u);else if(window.sessionStorage.getItem(u)!==window.location.pathname){window.sessionStorage.setItem(u,window.location.pathname),window.location.reload();return}return ei(e),()=>{S.current?.stop()}},[]),(0,i.useEffect)(()=>{let e=R.current;if(!e)return;let t=()=>{let t=Array.from(e.querySelectorAll("[data-tutorial-section]")),n=e.scrollHeight-e.clientHeight,a=Math.max(0,n-e.scrollTop);I(n<=0?1:Math.min(1,Math.max(0,e.scrollTop/n))),A(e=>n<=1||a<=(e?72:2));let s=e.getBoundingClientRect().top,i=t[0]?.dataset.tutorialSection??"intro";for(let e of t)if(e.getBoundingClientRect().top-s<=180)i=e.dataset.tutorialSection??i;else break;ec(i)};return t(),e.addEventListener("scroll",t,{passive:!0}),()=>e.removeEventListener("scroll",t)},[ec]),(0,i.useEffect)(()=>{if(!E)return;let e=e=>{P.current&&!P.current.contains(e.target)&&O(!1)},t=e=>{"Escape"===e.key&&O(!1)};return document.addEventListener("pointerdown",e),document.addEventListener("keydown",t),()=>{document.removeEventListener("pointerdown",e),document.removeEventListener("keydown",t)}},[E]),(0,i.useEffect)(()=>{let e=T.current;if(!e)return;let t=new IntersectionObserver(([e])=>D(e.isIntersecting),{threshold:.2});return t.observe(e),()=>t.disconnect()},[]),(0,i.useEffect)(()=>{R.current?.querySelectorAll("[data-tutorial-section]").forEach(e
117=>{e.dataset.active=String(e.dataset.tutorialSection===B)})},[B]);let ed=(0,i.useMemo)(()=>Q?[["WASM runtime","running"===U?"Running":"Stopped"],["Public peer","Not connected"],["Message",Q]]:z?[["WASM runtime","Running"],["Public peer",G>0?`${G} connected`:"connect"===J?"Connecting":"Not connected"],["Network","Fiber Testnet"]]:[["Runtime","Fiber WASM"],["Storage","IndexedDB"],["Transport","WSS"]],[J,Q,z,U,G]),eu=(0,i.useCallback)(async()=>{if(!J&&!S.current){Y("start"),Z(""),V(0);try{if(!window.crossOriginIsolated)throw Error("Cross-origin isolation is not enabled for this page.");let{FiberBrowserNode:t,RawKeyCredentialProvider:n}=await e.A(48754),a=function(){let e=window.localStorage.getItem(d);if(e)try{let t=JSON.parse(e);if(/^[0-9a-f]{64}$/i.test(t.fiberKey)&&/^[0-9a-f]{64}$/i.test(t.ckbKey))return{fiberKey:v(t.fiberKey),ckbKey:v(t.ckbKey),identifier:t.identifier}}catch{window.localStorage.removeItem(d)}let t={fiberKey:g(crypto.getRandomValues(new Uint8Array(32))),ckbKey:g(crypto.getRandomValues(new Uint8Array(32))),identifier:`fiber-quickstart-${crypto.randomUUID()}`};return window.localStorage.setItem(d,JSON.stringify(t)),{fiberKey:v(t.fiberKey),ckbKey:v(t.ckbKey),identifier:t.identifier}}(),s=new n(a.fiberKey,a.ckbKey,a.identifier),i=new t({network:"testnet",credential:s,nodeConfig:{bootnodes:[],logLevel:"info"}});i.on("stateChange",e=>q(e)),i.on("error",e=>Z(e.message)),S.current=i;let r=await i.start();X(r)}catch(e){Z(e instanceof Error?e.message:"Unable to start the Fiber node."),S.current?.isRunning||(S.current=null)}finally{Y(null)}}},[J]),eh=(0,i.useCallback)(async()=>{let e=S.current;if(!J&&e?.isRunning&&!(G>0)){Y("connect"),Z("");try{await e.connectPeer({address:o,pubkey:c});let t=(await e.listPeers()).peers;for(let n=0;0===t.length&&n<10;n+=1)await new Promise(e=>window.setTimeout(e,800)),t=(await e.listPeers()).peers;if(0===t.length)throw Error("The node is running, but the peer handshake timed out.");V(t.length)}catch(e){Z(e instanceof Error?e.message:"Unable to connect to the public peer.")}finally{Y(null)}}},[J,G]),ep=(0,i.useCallback)(async()=>{await navigator.clipboard.writeText(er.code),et(!0),window.setTimeout(()=>et(!1),1500)},[er]),ef=(0,i.useCallback)(async()=>{try{await navigator.clipboard.writeText(h),ea(!0),window.setTimeout(()=>ea(!1),1500)}catch{Z("Unable to copy the SDK installation command.")}},[]),em=(0,i.useCallback)(()=>{let e=K?k.current:T.current;e?.scrollIntoView({behavior:"smooth",block:"start"})},[K]);return(0,t.jsxs)("div",{className:l.default.shell,children:[(0,t.jsxs)("header",{className:l.default.tutorialToolbar,children:[(0,t.jsxs)("div",{className:l.default.tutorialSelect,ref:P,children:[(0,t.jsxs)("button",{"aria-expanded":E,"aria-haspopup":"menu",className:l.default.tutorialSelectButton,onClick:()=>O(e=>!e),type:"button",children:[(0,t.jsxs)("span",{children:["Tutorial: ",p[f].shortTitle]}),(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",className:l.default.selectChevron,height:18,src:"/icon-chevron-down.svg",width:18})]}),E&&(0,t.jsx)("div",{className:l.default.tutorialMenu,role:"menu",children:p.map((e,s)=>(0,t.jsxs)(a.default,{"aria-current":s===f?"page":void 0,className:s===f?l.default.tutorialMenuCurrent:void 0,href:e.href,onClick:()=>O(!1),role:"menuitem",children:[(0,t.jsx)("span",{children:e.shortTitle}),s===f&&(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",height:18,src:"/icon-checkmark.svg",width:18})]},e.href))})]}),(0,t.jsxs)("button",{className:l.default.runDemoAction,onClick:em,type:"button",children:[K?"View code walkthrough":"Try live demo"," ",(0,t.jsx)("span",{"aria-hidden":"true",children:K?"â":"â"})]}),(0,t.jsx)("div",{className:l.default.toolbarActions,children:(0,t.jsxs)("a",{download:!0,href:"/downloads/fiber-connect-wasm-node.zip",children:[(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",height:18,src:"/icon-download.svg",width:18}),"Download project"]})}),(0,t.jsx)("div",{className:l.default.horizontalProgress,"aria-hidden":"true",children:(0,t.jsx)("span",{className:l.default.progressFill,style:{transform:`scaleX(${F})`}})})]}),(0,t.jsxs)("section",{className:l.default.liveDemo,ref:T,children:[(0,t.jsx)("div",{className:l.default.liveDemoHeading,children:(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Interactive tutorial · About 15 minutes"}),(0,t.jsx)("h1",{children:"Run a Fiber WASM Node in Your Browser"}),(0,t.jsx)("p",{children:"Start a real Fiber node in this page, connect it to a public Fiber Testnet peer, and inspect its runtime state. No installation, wallet, Testnet funds, or backend is required."})]})}),(0,t.jsxs)("div",{className:l.default.demoSteps,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"1"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("strong",{children:"Start your browser node."})," Select"," ",(0,t.jsx)("b",{children:"Start WASM node"}),", then wait for ",(0,t.jsx)("b",{children:"Node running"})," and a public key. The node starts locally and remains disconnected."]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"2"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("strong",{children:"Connect to Fiber Testnet."})," Select"," ",(0,t.jsx)("b",{children:"Connect public peer"}),", then wait for the public-peer status to show a successful connection."]})]})]}),(0,t.jsxs)("div",{className:l.default.liveDemoGrid,children:[(0,t.jsxs)("div",{className:l.default.nodeCard,children:[(0,t.jsxs)("div",{className:l.default.nodeCardTop,children:[(0,t.jsx)("div",{className:l.default.nodeMark,children:"F"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Your browser node"}),(0,t.jsx)("strong",{children:N(U)})]}),(0,t.jsx)("i",{className:`${l.default.statusDot} ${el}`})]}),(0,t.jsxs)("div",{className:l.default.nodeIdentity,children:[(0,t.jsx)("span",{children:"Node pubkey"}),(0,t.jsx)("code",{children:w(z?.pubkey)})]}),(0,t.jsx)("div",{className:l.default.nodeStats,children:ed.map(([e,n])=>(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:e}),(0,t.jsx)("strong",{title:n,children:n})]},e))}),(0,t.jsxs)("div",{className:l.default.nodeActions,children:[(0,t.jsxs)("button",{className:l.default.startButton,disabled:null!==J||!!z,onClick:!1===es?()=>window.location.reload():eu,type:"button",children:["start"===J?"Starting Fiberâ¦":z?"Node running":!1===es?"Reload to enable WASM":"Start WASM node",(0,t.jsx)("span",{children:"â"})]}),(0,t.jsxs)("button",{className:l.default.connectButton,disabled:null!==J||!z||G>0,onClick:eh,type:"button",children:["connect"===J?"Connectingâ¦":G>0?`${G} peer connected`:"Connect public peer",(0,t.jsx)("span",{children:"â"})]})]})]}),(0,t.jsxs)("div",{className:l.default.eventPanel,children:[(0,t.jsxs)("div",{className:l.default.eventPanelHeader,children:[(0,t.jsx)("span",{children:"Node events"}),(0,t.jsx)("i",{className:`${l.default.liveDot} ${eo}`})]}),(0,t.jsxs)("div",{className:l.default.eventList,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:"01"}),(0,t.jsx)("code",{children:"wasm_runtime"}),(0,t.jsx)("span",{children:es?"ready":"checking"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:"02"}),(0,t.jsx)("code",{children:"node_state"}),(0,t.jsx)("span",{children:N(U)})]}),z&&(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:"03"}),(0,t.jsx)("code",{children:"node_started"}),(0,t.jsx)("span",{children:w(z.pubkey)})]}),G>0&&(0,t.jsxs)("div",{className:l.default.eventConnected,children:[(0,t.jsx)("time",{children:"04"}),(0,t.jsx)("code",{children:"peer_connected"}),(0,t.jsxs)("span",{children:[G," public peer"]})]}),Q&&(0,t.jsxs)("div",{className:l.default.eventError,children:[(0,t.jsx)("time",{children:"!"}),(0,t.jsx)("code",{children:"error"}),(0,t.jsx)("span",{children:Q})]})]})]})]})]}),(0,t.jsx)("div",{"aria-hidden":"true",className:l.default.sectionDivider}),(0,t.jsxs)("header",{className:l.default.walkthroughHeader,ref:k,children:[(0,t.jsx)("span",{children:"Implementation walkthrough"}),(0,t.jsx)("h2",{children:"Behind the scenes"}),(0,t.jsx)("p",{children:"See how the browser runs the node, why it needs browser isolation, and which Fiber SDK APIs power each action in the demo."})]}),(0,t.jsxs)("section",{className:l.default.instructionWorkspace,children:[(0,t.jsxs)("div",{className:l.default.instructionGrid,children:[(0,t.jsxs)("div",{className:l.default.article,onClick:e=>{let t=e.target.closest("[data-tutorial-section]");t?.dataset.tutorialSection&&ec(t.dataset.tutorialSection)},ref:R,children:[(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"architecture",children:[(0,t.jsx)("div",{className:l.default.stepLabel,children:"Architecture"}),(0,t.jsx)("h2",{children:"How the browser node works"}),(0,t.jsxs)("p",{children:["The page starts the WebAssembly version of Fiber in"," ",(0,t.jsx)(r.ExplainedTerm,{explanation:"Backgroun
117d browser execution contexts that keep the node runtime from blocking the page interface.",id:"worker-definition",children:"Web Workers"}),". The workers exchange data through"," ",(0,t.jsx)(r.ExplainedTerm,{explanation:"A block of memory that multiple workers can access to coordinate their work.",id:"shared-array-buffer-definition",children:"SharedArrayBuffer"}),", while"," ",(0,t.jsx)(r.ExplainedTerm,{explanation:"Browser storage used to keep the node's local data.",id:"indexed-db-definition",children:"IndexedDB"})," ","stores local node data. The node reaches the public Fiber peer over"," ",(0,t.jsx)(r.ExplainedTerm,{explanation:"WebSocket Secure: an encrypted, persistent, two-way connection supported by browsers.",id:"wss-definition",children:"WSS"}),"."]}),(0,t.jsxs)("div",{className:l.default.flow,"aria-label":"Browser to Fiber network flow",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("b",{children:"Browser application"}),(0,t.jsx)("span",{children:"React + Fiber WASM + IndexedDB"})]}),(0,t.jsx)("span",{className:l.default.flowArrow,children:"â WSS â"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("b",{children:"Public Fiber peer"}),(0,t.jsx)("span",{children:"Testnet network"})]})]}),(0,t.jsxs)("p",{className:l.default.followupParagraph,children:[(0,t.jsx)("code",{children:"@fiber-pay/sdk"})," provides the browser-facing APIs used here to create the node, manage its identity, observe its state, and connect it to peers. Install it in an existing TypeScript or JavaScript project with:"]}),(0,t.jsxs)("div",{className:l.default.setupCodeBlock,children:[(0,t.jsxs)("div",{className:l.default.setupCodeHeader,children:[(0,t.jsx)("span",{children:"Terminal"}),(0,t.jsxs)("button",{"aria-label":en?"SDK installation command copied":"Copy SDK installation command",onClick:ef,type:"button",children:[(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",height:18,src:en?"/icon-checkmark.svg":"/icon-copy.svg",width:18}),en?"Copied":"Copy"]})]}),(0,t.jsx)("pre",{children:(0,t.jsx)("code",{children:h})})]})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"isolate",children:[(0,t.jsx)("div",{className:l.default.stepLabel,children:"Browser requirement"}),(0,t.jsx)("h2",{children:"Why the page needs COOP and COEP"}),(0,t.jsxs)("p",{children:["Browsers restrict shared memory to pages that opt into cross-origin isolation. This example enables it with two response headers:"," ",(0,t.jsx)(r.ExplainedTerm,{explanation:"Cross-Origin-Opener-Policy separates this page from windows opened from other origins.",id:"coop-definition",children:"COOP"})," ","and"," ",(0,t.jsx)(r.ExplainedTerm,{explanation:"Cross-Origin-Embedder-Policy permits only cross-origin resources that explicitly allow the page to load them.",id:"coep-definition",children:"COEP"}),"."]}),(0,t.jsxs)("p",{className:l.default.followupParagraph,children:["Together, these headers make"," ",(0,t.jsx)("code",{children:"window.crossOriginIsolated"})," return ",(0,t.jsx)("code",{children:"true"}),", allowing Fiber WASM to use ",(0,t.jsx)("code",{children:"SharedArrayBuffer"}),"."]}),(0,t.jsxs)("div",{className:l.default.checkList,children:[(0,t.jsx)("span",{children:"Cross-Origin-Opener-Policy: same-origin"}),(0,t.jsx)("span",{children:"Cross-Origin-Embedder-Policy: require-corp"})]}),(0,t.jsxs)("div",{className:`${l.default.note} ${l.default.noteInfo}`,children:[(0,t.jsx)("strong",{children:"The live demo is already configured"}),(0,t.jsx)("p",{children:"You only need to add these headers when running the project on your own server."})]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"next.config.mjs · lines 1â24"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"start",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"1"})," Code walkthrough"]}),(0,t.jsx)("h2",{children:"Start the node without connecting"}),(0,t.jsxs)("p",{children:["Before creating the node, verify that"," ",(0,t.jsx)("code",{children:"crossOriginIsolated"})," is enabled. Then create a credential and initialize a Fiber Testnet node with an empty"," ",(0,t.jsx)("code",{children:"bootnodes"})," list so it starts without connecting to a peer. Register the ",(0,t.jsx)("code",{children:"stateChange"})," listener before calling"," ",(0,t.jsx)("code",{children:"node.start()"})," to receive startup updates."]}),(0,t.jsxs)("div",{className:`${l.default.note} ${l.default.noteWarning}`,children:[(0,t.jsx)("strong",{children:"Testnet only"}),(0,t.jsx)("p",{children:"The generated credentials are intended for Testnet development only. Production applications must store private keys in encrypted storage and provide backup, account-switching, and recovery mechanisms."})]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"lib/fiber.ts · lines 17â49"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"connect-helper",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"2"})," Code walkthrough"]}),(0,t.jsx)("h2",{children:"Connect the node to a public peer"}),(0,t.jsxs)("p",{children:["The browser node needs both an address and a pubkey to make its first connection. The ",(0,t.jsx)("code",{children:"address"})," tells it how to reach the public Fiber Testnet peer over WSS; the ",(0,t.jsx)("code",{children:"pubkey"}
117)," ","identifies the Fiber node it expects to reach."]}),(0,t.jsxs)("div",{className:l.default.peerConnectionInputs,children:[(0,t.jsxs)("div",{className:l.default.connectionInput,children:[(0,t.jsxs)("div",{className:l.default.connectionInputHeader,children:[(0,t.jsx)("code",{children:"address"}),(0,t.jsx)("span",{children:"Browser route"})]}),(0,t.jsx)("code",{className:l.default.connectionValue,children:o}),(0,t.jsxs)("div",{"aria-label":"Address structure",className:l.default.addressPartsGrid,children:[(0,t.jsxs)("div",{className:l.default.addressPart,children:[(0,t.jsx)("code",{children:"/dns4/bottle.fiber.channel"}),(0,t.jsx)("span",{className:l.default.codeComment,children:"// DNS host"})]}),(0,t.jsxs)("div",{className:l.default.addressPart,children:[(0,t.jsx)("code",{children:"/tcp/443"}),(0,t.jsx)("span",{className:l.default.codeComment,children:"// TCP port"})]}),(0,t.jsxs)("div",{className:l.default.addressPart,children:[(0,t.jsx)("code",{children:"/wss"}),(0,t.jsx)("span",{className:l.default.codeComment,children:"// encrypted WebSocket"})]}),(0,t.jsxs)("div",{className:l.default.addressPart,children:[(0,t.jsx)("code",{children:"/p2p/Qm..."}),(0,t.jsx)("span",{className:l.default.codeComment,children:"// libp2p peer ID"})]})]})]}),(0,t.jsxs)("div",{className:l.default.connectionInput,children:[(0,t.jsxs)("div",{className:l.default.connectionInputHeader,children:[(0,t.jsx)("code",{children:"pubkey"}),(0,t.jsx)("span",{children:"Fiber identity"})]}),(0,t.jsx)("code",{className:l.default.connectionValue,children:c}),(0,t.jsx)("span",{className:l.default.codeComment,children:"// Hex-encoded secp256k1 Fiber node public key"})]})]}),(0,t.jsxs)("p",{className:l.default.followupParagraph,children:[(0,t.jsx)("code",{children:"connectToRouter(node)"})," passes both values to"," ",(0,t.jsxs)("code",{children:["node.connectPeer(","{ address, pubkey }",")"]}),", allowing the first connection to use the explicit WSS route without depending on graph synchronization."]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"lib/fiber.ts · lines 51â56"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"react-store",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"3.1"})," React integration"]}),(0,t.jsx)("h2",{children:"Keep the node between renders"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"useRef()"})," keeps one node instance available across React renders without triggering extra renders. React state stores the status, public key, and peer count displayed by the interface."]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"app/fiber/page.tsx · lines 7â11"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"react-start",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"3.2"})," React integration"]}),(0,t.jsx)("h2",{children:"Start the WASM node"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"start()"})," calls ",(0,t.jsx)("code",{children:"startFiber()"}),", saves the returned node in a React ref, then calls"," ",(0,t.jsx)("code",{children:"getNodeInfo()"})," and stores the public key in React state. It does not connect to a peer."]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"app/fiber/page.tsx · lines 13â18"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"react-connect",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"3.3"})," React integration"]}),(0,t.jsx)("h2",{children:"Connect the running node to Testnet"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"connect()"})," calls ",(0,t.jsx)("code",{children:"connectToRouter()"}),", then calls ",(0,t.jsx)("code",{children:"listPeers()"})," and stores the connected-peer count in React state so the interface can confirm the connection."]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"app/fiber/page.tsx · lines 20â25"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"react-render",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"3.4"})," React integration"]}),(0,t.jsx)("h2",{children:"Show the result"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"FiberPage()"})," gives users visible confirmation of each operation. It binds the handlers to
117separate buttons and displays the node status, public key, and peer count."]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"app/fiber/page.tsx · lines 27â39"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"download",children:[(0,t.jsx)("div",{className:l.default.stepLabel,children:"Optional local setup"}),(0,t.jsx)("h2",{children:"Run the complete project locally"}),(0,t.jsxs)("p",{children:["Select ",(0,t.jsx)("strong",{children:"Download project"})," in the top-right corner, or"," ",(0,t.jsx)("a",{className:l.default.inlineDownloadLink,download:!0,href:"/downloads/fiber-connect-wasm-node.zip",children:"download here"}),". The archive already contains the Next.js application, Fiber integration, browser headers, and interface shown here. After extracting it, open the project directory and run:"]}),(0,t.jsxs)("div",{className:l.default.setupCodeBlock,children:[(0,t.jsx)("div",{className:l.default.setupCodeHeader,children:(0,t.jsx)("span",{children:"Terminal"})}),(0,t.jsx)("pre",{children:(0,t.jsx)("code",{children:`npm install 118npm run dev`})})]}),(0,t.jsxs)("p",{className:l.default.followupParagraph,children:["Then open ",(0,t.jsx)("code",{children:"http://localhost:3000/fiber"})," in your browser."]})]})]}),(0,t.jsxs)("aside",{className:l.default.codePanel,"aria-label":"Tutorial project files",children:[(0,t.jsx)("div",{className:l.default.fileTabs,role:"tablist","aria-label":"Code files",children:b.map(e=>(0,t.jsx)("button",{"aria-selected":e.id===L,className:e.id===L?l.default.activeTab:"",onClick:()=>{H(e.id),M(null)},role:"tab",type:"button",children:e.label},e.id))}),(0,t.jsx)(C,{file:er,focus:W}),(0,t.jsxs)("div",{className:l.default.codeMeta,children:[(0,t.jsxs)("span",{children:[er.label," ",(0,t.jsx)("i",{children:er.language})]}),(0,t.jsxs)("button",{"aria-label":ee?`${er.label} copied`:`Copy ${er.label}`,className:l.default.copyButton,onClick:ep,type:"button",children:[(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",height:18,src:ee?"/icon-checkmark.svg":"/icon-copy.svg",width:18}),ee?"Copied":"Copy file"]})]})]})]}),$&&(0,t.jsxs)("nav",{"aria-label":"Tutorial navigation",className:`${l.default.tutorialFooter} ${l.default.walkthroughFooter}`,children:[(0,t.jsxs)("button",{onClick:()=>y.push(m.href),type:"button",children:[(0,t.jsx)("span",{"aria-hidden":"true",children:"â"})," Previous"]}),(0,t.jsxs)("button",{disabled:!x,onClick:()=>x&&y.push(x.href),type:"button",children:["Next ",(0,t.jsx)("span",{"aria-hidden":"true",children:"â"})]})]})]})]})}])},19427,e=>{"use strict";var t=e.i(99873),n=e.i(56615),a=e.i(21239),s=e.i(41178),i=e.i(15300),r=e.i(42829),l=e.i(84678);let o="0x02b6d4e3ab86a2ca2fad6fae0ecb2e1e559e0b911939872a90abdda6d20302be71",c="fiber-docs:wasm-quickstart-profile-v1",d="fiber-docs:wasm-isolation-reload-v1",u=[{shortTitle:"Connect a WASM node",href:"/docs/build/connect-wasm-node"},{shortTitle:"Open a channel and send a payment",href:"/docs/build/open-channel-payment"},{shortTitle:"Send a multi-hop invoice payment",href:"/docs/build/multi-hop-invoice"},{shortTitle:"Open a unidirectional channel",href:"/docs/build/unidirectional-channel"},{shortTitle:"Build a Hold Invoice payment",href:"/docs/build/hold-invoice"},{shortTitle:"Unlock encrypted data with a payment",href:"/docs/build/encrypted-data-payment"},{shortTitle:"Pay for a verified result",href:"/docs/build/verified-result-payment"},{shortTitle:"Pay with RUSD",href:"/docs/build/rusd-payment"},{shortTitle:"Close a channel and recover funds",href:"/docs/build/close-channel"}],h=[{id:"fiber",label:"lib/fiber.ts",language:"typescript",code:`import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 119 120const routerAddress = 121 '/dns4/bottle.fiber.channel/tcp/443/wss/p2p/' + 122 'QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo'; 123 124export const routerPubkey = 125 '0x02b6d4e3ab86a2ca2fad6fae0ecb2e1e559e0b911939872a90abdda6d20302be71'; 126 127const profileKey = 'fiber-docs:wasm-quickstart-profile-v1'; 128 129function bytesToHex(bytes: Uint8Array) { 130 return Array.from(bytes, (value) => 131 value.toString(16).padStart(2, '0'), 132 ).join(''); 133} 134 135function hexToBytes(value: string) { 136 return Uint8Array.from(value.match(/.{2}/g) ?? [], (part) => 137 Number.parseInt(part, 16), 138 ); 139} 140 141function getOrCreateProfile() { 142 const saved = localStorage.getItem(profileKey); 143 if (saved) { 144 const profile = JSON.parse(saved); 145 return { 146 fiberKey: hexToBytes(profile.fiberKey), 147 ckbKey: hexToBytes(profile.ckbKey), 148 identifier: profile.identifier, 149 }; 150 } 151 152 const profile = { 153 fiberKey: bytesToHex(crypto.getRandomValues(new Uint8Array(32))), 154 ckbKey: bytesToHex(crypto.getRandomValues(new Uint8Array(32))), 155 identifier: \`fiber-quickstart-\${crypto.randomUUID()}\`, 156 }; 157 localStorage.setItem(profileKey, JSON.stringify(profile)); 158 return { 159 fiberKey: hexToBytes(profile.fiberKey), 160 ckbKey: hexToBytes(profile.ckbKey), 161 identifier: profile.identifier, 162 }; 163} 164 165export async function startFiber(): Promise<FiberBrowserNode> { 166 if (!crossOriginIsolated) { 167 throw new Error('Cross-origin isolation is required'); 168 } 169 170 const { 171 FiberBrowserNode, 172 RawKeyCredentialProvider, 173 } = await import('@fiber-pay/sdk/browser'); 174 const profile = getOrCreateProfile(); 175 const credential = new RawKeyCredentialProvider( 176 profile.fiberKey, 177 profile.ckbKey, 178 profile.identifier, 179 ); 180 const node = new FiberBrowserNode({ 181 network: 'testnet', 182 credential, 183 nodeConfig: { bootnodes: [], logLevel: 'info' }, 184 }); 185 186 await node.start(); 187 return node; 188} 189 190export async function connectToRouter(node: FiberBrowserNode) { 191 await node.connectPeer({ 192 address: routerAddress, 193 pubkey: routerPubkey, 194 }); 195}`},{id:"balance",label:"lib/balance.ts",language:"typescript",code:`type FundingScript = { 196 code_hash: string; 197 hash_type: string; 198 args: string; 199}; 200 201export async function queryCkbBalance(script: FundingScript) { 202 const response = await fetch('https://testnet.ckbapp.dev/', { 203 method: 'POST', 204 headers: { 'Content-Type': 'application/json' }, 205 body: JSON.stringify({ 206 jsonrpc: '2.0', 207 id: 1, 208 method: 'get_cells_capacity', 209 params: [{ script, script_type: 'lock' }
209], 210 }), 211 }); 212 const payload = await response.json(); 213 return BigInt(payload.result.capacity); 214} 215 216export function watchCkbBalance( 217 script: FundingScript, 218 onBalance: (value: bigint) => void, 219) { 220 let checking = false; 221 const check = async () => { 222 if (checking || document.visibilityState !== 'visible') return; 223 checking = true; 224 try { 225 onBalance(await queryCkbBalance(script)); 226 } finally { 227 checking = false; 228 } 229 }; 230 231 void check(); 232 const timer = window.setInterval(check, 5_000); 233 document.addEventListener('visibilitychange', check); 234 235 return () => { 236 window.clearInterval(timer); 237 document.removeEventListener('visibilitychange', check); 238 }; 239}`},{id:"amounts",label:"lib/amounts.ts",language:"typescript",code:`const SHANNONS_PER_CKB = 100_000_000n; 240 241export function ckbToHex(value: string) { 242 if (!/^\\d+(\\.\\d{1,8})?$/.test(value)) { 243 throw new Error('Enter a valid CKB amount'); 244 } 245 246 const [whole, fraction = ''] = value.split('.'); 247 const shannons = 248 BigInt(whole) * SHANNONS_PER_CKB + 249 BigInt(fraction.padEnd(8, '0')); 250 251 if (shannons <= 0n) { 252 throw new Error('Amount must be positive'); 253 } 254 255 return \`0x\${shannons.toString(16)}\` as \`0x\${string}\`; 256}`},{id:"channel",label:"lib/channel.ts",language:"typescript",code:`import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 257import { ckbToHex } from './amounts'; 258 259const wait = (ms: number) => 260 new Promise((resolve) => setTimeout(resolve, ms)); 261 262export async function openCkbChannel( 263 node: FiberBrowserNode, 264 peerPubkey: \`0x\${string}\`, 265 amount: string, 266) { 267 const result = await node.openChannel({ 268 pubkey: peerPubkey, 269 funding_amount: ckbToHex(amount), 270 public: true, 271 }); 272 273 return result.temporary_channel_id; 274} 275 276export function watchChannelStates( 277 node: FiberBrowserNode, 278 onState: (state: string) => void, 279) { 280 let active = true; 281 282 const poll = async () => { 283 while (active) { 284 const { channels } = await node.listChannels(); 285 const latest = channels.at(-1); 286 if (latest) onState(latest.state.state_name); 287 await wait(2_000); 288 } 289 }; 290 291 void poll(); 292 return () => { 293 active = false; 294 }; 295}`},{id:"payment",label:"lib/payment.ts",language:"typescript",code:`import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 296import { ckbToHex } from './amounts'; 297 298export async function sendKeysend( 299 node: FiberBrowserNode, 300 peerPubkey: \`0x\${string}\`, 301 amount: string, 302) { 303 const submitted = await node.sendPayment({ 304 target_pubkey: peerPubkey, 305 amount: ckbToHex(amount), 306 keysend: true, 307 }); 308 309 if ( 310 submitted.status === 'Success' || 311 submitted.status === 'Failed' 312 ) { 313 return submitted; 314 } 315 316 return node.waitForPayment(submitted.payment_hash, { 317 timeout: 30_000, 318 interval: 1_000, 319 }); 320}`},{id:"app",label:"app/pay/page.tsx",language:"tsx",code:`'use client'; 321 322import { useRef, useState } from 'react'; 323import { 324 scriptToAddress, 325 type FiberBrowserNode, 326} from '@fiber-pay/sdk/browser'; 327import { 328 connectToRouter, 329 routerPubkey, 330 startFiber, 331} from '@/lib/fiber'; 332import { openCkbChannel } from '@/lib/channel'; 333import { sendKeysend } from '@/lib/payment'; 334 335export default function PayPage() { 336 const node = useRef<FiberBrowserNode | null>(null); 337 const [connected, setConnected] = useState(false); 338 const [fundingAddress, setFundingAddress] = useState(''); 339 const [channelReady, setChannelReady] = useState(false); 340 const [paymentStatus, setPaymentStatus] = useState('Not sent'); 341 342 async function start() { 343 // Start this browser's Fiber node. 344 const fiber = await startFiber(); 345 node.current = fiber; 346 const info = await fiber.getNodeInfo(); 347 setFundingAddress( 348 scriptToAddress(info.default_funding_lock_script, 'testnet'), 349 ); 350 } 351 352 async function connect() { 353 if (!node.current) return; 354 await connectToRouter(node.current); 355 setConnected(true); 356 } 357 358 async function openChannel() { 359 if (!node.current || !connected) return; 360 await openCkbChannel(node.current, routerPubkey, '499'); 361 setChannelReady(true); 362 } 363 364 async function pay() { 365 if (!node.current || !channelReady) return; 366 const result = await sendKeysend(node.current, routerPubkey, '1'); 367 setPaymentStatus(result.status); 368 } 369 370 return ( 371 <main> 372 <button onClick={start}>Start node</button> 373 <button disabled={!node.current} onClick={connect}> 374 Connect peer 375 </button> 376 <code>{fundingAddress}</code> 377 <button disabled={!connected} onClick={openChannel}> 378 Open 499 CKB channel 379 </button> 380 <button disabled={!channelReady} onClick={pay}> 381 Send 1 CKB 382 </button> 383 <p>{paymentStatus}</p> 384 </main> 385 ); 386}`}],p={model:{file:"app",start:39,end:48},identity:{file:"app",start:23,end:31},fund:{file:"balance",start:7,end:45},amounts:{file:"amounts",start:1,end:18},open:{file:"channel",start:7,end:19},ready:{file:"channel",start:21,end:40}
386,pay:{file:"payment",start:4,end:26},react:{file:"app",start:16,end:65}},f=/(\/\/.*$|\/\*.*?\*\/|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`|<\/?[A-Za-z][\w.]*|\b(?:import|from|export|default|const|let|var|function|async|await|return|if|else|throw|new|typeof|instanceof|true|false|null|undefined|type|interface|extends|implements|as|in|of)\b|\b(?:[A-Z][A-Za-z0-9_]*|[a-zA-Z_$][\w$]*(?=\())\b|\b\d[\d_]*(?:\.\d+)?n?\b)/g;function m({file:e,focus:n}){let a=(0,i.useRef)(null);return(0,i.useEffect)(()=>{if(!n)return;let e=a.current,t=e?.querySelector(`[data-line="${n.start}"]`);if(!e||!t)return;let s=t.getBoundingClientRect().top-e.getBoundingClientRect().top+e.scrollTop;e.scrollTo({top:Math.max(0,s-.28*e.clientHeight),behavior:"smooth"})},[e.id,n]),(0,t.jsx)("pre",{className:l.default.code,"aria-label":`${e.label} code`,ref:a,children:(0,t.jsx)("code",{children:e.code.split("\n").map((a,s)=>{let i=s+1,r=n?.file===e.id&&i>=n.start&&i<=n.end;return(0,t.jsxs)("span",{className:`${l.default.codeLine} ${r?l.default.focusedCodeLine:""}`,"data-line":i,children:[(0,t.jsx)("span",{className:l.default.lineNumber,children:i}),(0,t.jsx)("span",{children:a?function(e){let n=[],a=0;for(let i of e.matchAll(f)){var s;let r=i.index??0;r>a&&n.push(e.slice(a,r)),n.push((0,t.jsx)("span",{className:(s=i[0]).startsWith("//")||s.startsWith("/*")?l.default.tokenComment:/^['"`]/.test(s)?l.default.tokenString:s.startsWith("<")?l.default.tokenTag:/^\d/.test(s)?l.default.tokenNumber:/^[A-Z]/.test(s)?l.default.tokenType:/^(true|false|null|undefined)$/.test(s)?l.default.tokenConstant:/^(import|from|export|default|const|let|var|function|async|await|return|if|else|throw|new|typeof|instanceof|type|interface|extends|implements|as|in|of)$/.test(s)?l.default.tokenKeyword:l.default.tokenFunction,children:i[0]},`${r}-${i[0]}`)),a=r+i[0].length}return a<e.length&&n.push(e.slice(a)),n}(a):" "})]},`${e.id}-${s}`)})})})}function x(e){return Array.from(e,e=>e.toString(16).padStart(2,"0")).join("")}function b(e){return Uint8Array.from(e.match(/.{2}/g)??[],e=>Number.parseInt(e,16))}function j(e){let t=e.trim();if(!/^\d+(\.\d{1,8})?$/.test(t))throw Error("Enter a valid CKB amount with at most 8 decimals.");let[n,a=""]=t.split("."),s=100000000n*BigInt(n)+BigInt(a.padEnd(8,"0"));if(s<=0n)throw Error("Amount must be greater than zero.");return`0x${s.toString(16)}`}async function y(e){let t=await fetch("https://testnet.ckbapp.dev/",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:1,method:"get_cells_capacity",params:[{script:e,script_type:"lock"}]})}),n=await t.json();if(!n.result?.capacity)throw Error(n.error?.message??"Unable to query the CKB balance.");return BigInt(n.result.capacity)}function g(e){if(null===e)return"â";let t=(e%100000000n).toString().padStart(8,"0").replace(/0+$/,"");return`${e/100000000n}${t?`.${t}`:""} CKB`}function v(e){return e?e.length>28?`${e.slice(0,14)}â¦${e.slice(-10)}`:e:"â"}function w(e){return"running"===e?"Node running":"starting"===e?"Starting WASM":"stopping"===e?"Stopping":"error"===e?"Connection issue":"Ready to start"}function N({children:e,id:n,label:a,reason:s}){return s?(0,t.jsx)(r.ExplainedTerm,{ariaLabel:a,className:l.default.disabledActionHint,explanation:s,id:n,children:e}):(0,t.jsx)(t.Fragment,{children:e})}function C(){return(0,t.jsx)("svg",{"aria-hidden":"true",className:l.default.refreshIcon,fill:"none",viewBox:"0 0 16 16",children:(0,t.jsx)("path",{d:"M13 5V2m0 0h-3m3 0-2.1 2.1A5 5 0 1 0 13 9"})})}let S=e=>new Promise(t=>window.setTimeout(t,e)),k=["NEGOTIATING_FUNDING","COLLABORATING_FUNDING_TX","SIGNING_COMMITMENT","AWAITING_TX_SIGNATURES","AWAITING_CHANNEL_READY","CHANNEL_READY"];e.s(["FiberChannelPaymentTutorial",0,function(){let f,T,R=(0,s.useRouter)(),P=(0,i.useRef)(null),B=(0,i.useRef)(null),_=(0,i.useRef)(null),F=(0,i.useRef)(null),I=(0,i.useRef)(null),[$,A]=(0,i.useState)("intro"),[E,O]=(0,i.useState)(0),[K,D]=(0,i.useState)(!1),[L,H]=(0,i.useState)(!1),[W,M]=(0,i.useState)(!1),[U,q]=(0,i.useState)("app"),[z,X]=(0,i.useState)(null),[G,V]=(0,i.useState)("idle"),[J,Y]=(0,i.useState)(null),[Q,Z]=(0,i.useState)(0),[ee,et]=(0,i.useState)(""),[en,ea]=(0,i.useState)(null),[es,ei]=(0,i.useState)("499"),[er,el]=(0,i.useState)("Not opened"),[eo,ec]=(0,i.useState)(0),[ed,eu]=(0,i.useState)([]),[eh,ep]=(0,i.useState)(0),[ef,em]=(0,i.useState)("1"),[ex,eb]=(0,i.useState)("Not sent"),[ej,ey]=(0,i.useState)(""),[eg,ev]=(0,i.useState)(""),[ew,eN]=(0,i.useState)(""),[eC,eS]=(0,i.useState)(null),[ek,eT]=(0,i.useState)(null),[eR,eP]=(0,i.useState)([]),[eB,e_]=(0,i.useState)(null),[eF,eI]=(0,i.useState)(""),[e$,eA]=(0,i.useState)(!1),[eE,eO]=(0,i.useState)(!1),[eK,eD]=(0,i.useState)(null),eL=(0,i.useCallback)(e=>{let t=e.filter(e=>e.state.state_name.toLowerCase().includes("ready")),n=e.at(-1)?.state.state_name,a=t.at(-1)??e.at(-1);(ec(e.length),ep(t.length),a&&eN(a.channel_id),n)?(el(n),eu(e=>e.at(-1)===n?e:[...e,n].slice(-8))):el(e=>e.startsWith("Opening")?e:"Not opened")},[]),eH=(0,i.useCallback)((e,t)=>{eP(n=>[...n.slice(-5),{at:new Date().toLocaleTimeString([],{hour12:!1}),message:e,tone:t}])},[]),eW=h.find(e=>e.id===U)??h[0],eM=(()=>
386{if(null===en)return!1;try{return en>=BigInt(j(es))}catch{return!1}})(),eU=0===eh&&"Not opened"!==er&&"Open failed"!==er,eq=(f=er.replace(/[^a-z0-9]/gi,"").toLowerCase(),(T=k.findIndex(e=>e.replace(/_/g,"").toLowerCase()===f))>=0&&T<k.length-1?k[T+1]:null),ez=(0,i.useCallback)(e=>{A(e);let t=p[e];t?(q(t.file),X(t)):X(null)},[]);(0,i.useEffect)(()=>{let e=window.crossOriginIsolated&&"u">typeof SharedArrayBuffer;if(e)window.sessionStorage.removeItem(d);else if(window.sessionStorage.getItem(d)!==window.location.pathname){window.sessionStorage.setItem(d,window.location.pathname),window.location.reload();return}return eD(e),()=>{P.current?.stop()}},[]),(0,i.useEffect)(()=>{let e=F.current;if(!e)return;let t=()=>{let t=Array.from(e.querySelectorAll("[data-tutorial-section]")),n=e.scrollHeight-e.clientHeight,a=Math.max(0,n-e.scrollTop);O(n<=0?1:Math.min(1,Math.max(0,e.scrollTop/n))),D(e=>n<=1||a<=(e?72:2));let s=e.getBoundingClientRect().top,i=t[0]?.dataset.tutorialSection??"intro";for(let e of t)if(e.getBoundingClientRect().top-s<=180)i=e.dataset.tutorialSection??i;else break;ez(i)};return t(),e.addEventListener("scroll",t,{passive:!0}),()=>e.removeEventListener("scroll",t)},[ez]),(0,i.useEffect)(()=>{F.current?.querySelectorAll("[data-tutorial-section]").forEach(e=>{e.dataset.active=String(e.dataset.tutorialSection===$)})},[$]),(0,i.useEffect)(()=>{if(!L)return;let e=e=>{I.current&&!I.current.contains(e.target)&&H(!1)},t=e=>{"Escape"===e.key&&H(!1)};return document.addEventListener("pointerdown",e),document.addEventListener("keydown",t),()=>{document.removeEventListener("pointerdown",e),document.removeEventListener("keydown",t)}},[L]),(0,i.useEffect)(()=>{let e=_.current;if(!e)return;let t=new IntersectionObserver(([e])=>M(e.isIntersecting),{threshold:.2});return t.observe(e),()=>t.disconnect()},[]),(0,i.useEffect)(()=>{if(!J)return;let e=!1,t=!1,n=async()=>{if(!e&&!t&&"visible"===document.visibilityState){t=!0;try{let t=await y(J.default_funding_lock_script);e||ea(t)}catch{}finally{t=!1}}},a=window.setInterval(()=>void n(),5e3),s=()=>{"visible"===document.visibilityState&&n()};return document.addEventListener("visibilitychange",s),()=>{e=!0,window.clearInterval(a),document.removeEventListener("visibilitychange",s)}},[J]),(0,i.useEffect)(()=>{if(!J)return;let e=!1,t=!1,n=async()=>{let n=P.current;if(!e&&!t&&n&&"visible"===document.visibilityState){t=!0;try{let{channels:t}=await n.listChannels();e||eL(t)}catch{}finally{t=!1}}};n();let a=window.setInterval(()=>void n(),2e3),s=()=>{"visible"===document.visibilityState&&n()};return document.addEventListener("visibilitychange",s),()=>{e=!0,window.clearInterval(a),document.removeEventListener("visibilitychange",s)}},[eL,J]);let eX=(0,i.useCallback)(async()=>{let e=P.current;if(!e)return[];let[{peers:t},n,{channels:a}]=await Promise.all([e.listPeers(),e.nodeInfo(),e.listChannels()]);return Z(t.length),Y(n),eL(a),ea(await y(n.default_funding_lock_script)),a},[eL]),eG=(0,i.useCallback)(async()=>{if(!(eB||P.current?.isRunning&&Q>0)){e_("prepare"),eI("");try{if(!window.crossOriginIsolated)throw Error("Cross-origin isolation is not enabled for this page.");let t=P.current,n=J;if(!t?.isRunning){let{FiberBrowserNode:a,RawKeyCredentialProvider:s}=await e.A(48754),i=function(){let e=window.localStorage.getItem(c);if(e)try{let t=JSON.parse(e);if(/^[0-9a-f]{64}$/i.test(t.fiberKey)&&/^[0-9a-f]{64}$/i.test(t.ckbKey))return{fiberKey:b(t.fiberKey),ckbKey:b(t.ckbKey),identifier:t.identifier}}catch{window.localStorage.removeItem(c)}let t={fiberKey:x(crypto.getRandomValues(new Uint8Array(32))),ckbKey:x(crypto.getRandomValues(new Uint8Array(32))),identifier:`fiber-quickstart-${crypto.randomUUID()}`};return window.localStorage.setItem(c,JSON.stringify(t)),{fiberKey:b(t.fiberKey),ckbKey:b(t.ckbKey),identifier:t.identifier}}(),r=new s(i.fiberKey,i.ckbKey,i.identifier);(t=new a({network:"testnet",credential:r,nodeConfig:{bootnodes:[],logLevel:"info"}})).on("stateChange",V),t.on("error",e=>eI(e.message)),P.current=t,n=await t.start()}n||(n=await t.nodeInfo()),Y(n);let{scriptToAddress:a}=await e.A(48754);et(a(n.default_funding_lock_script,"testnet"));try{ea(await y(n.default_funding_lock_script))}catch(e){eI(e instanceof Error?e.message:"Unable to read the Testnet balance.")}let{channels:s}=await t.listChannels();if(eL(s),0===Q){await t.connectPeer({address:"/dns4/bottle.fiber.channel/tcp/443/wss/p2p/QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo",pubkey:o});let e=(await t.listPeers()).peers;for(let n=0;0===e.length&&n<10;n+=1)await S(800),e=(await t.listPeers()).peers;if(0===e.length)throw Error("The public peer handshake timed out.");Z(e.length)}}catch(e){eI(e instanceof Error?e.message:"Unable to prepare the browser node."),P.current?.isRunning||(P.current=null)}finally{e_(null)}}},[eL,eB,J,Q]),eV=(0,i.useCallback)(async()=>
386{if(P.current&&!eB){e_("refresh"),eI("");try{await eX()}catch(e){eI(e instanceof Error?e.message:"Unable to refresh live state.")}finally{e_(null)}}},[eB,eX]),eJ=(0,i.useCallback)(async()=>{let e=P.current;if(e&&0!==Q&&!eB){e_("channel"),eI(""),el("Opening");try{let t=await e.openChannel({pubkey:o,funding_amount:j(es),public:!0});el(`Opening ${v(t.temporary_channel_id)}`);let{channels:n}=await e.listChannels();n.length&&eL(n)}catch(e){el("Open failed"),eI(e instanceof Error?e.message:"Unable to open the Testnet channel.")}finally{e_(null)}}},[eL,eB,es,Q]),eY=(0,i.useCallback)(async()=>{let e=P.current;if(e&&0!==eh&&0!==Q&&!eB){e_("payment"),eI(""),eb("Sending"),ey(""),ev(ef),eS(null),eT(null),eH(`Sending ${ef} CKB with keysendâ¦`,"pending");try{let t=(await e.listChannels()).channels,n=t.find(e=>e.channel_id===ew)??t.find(e=>e.state.state_name.toLowerCase().includes("ready"));eS(n?BigInt(n.local_balance):null);let a=await e.sendPayment({target_pubkey:o,amount:j(ef),keysend:!0});eH(`Submitted \xb7 ${v(a.payment_hash)}`,"pending"),"Success"!==a.status&&"Failed"!==a.status&&eH("Waiting for a terminal payment statusâ¦","pending");let s="Success"!==a.status&&"Failed"!==a.status?await e.waitForPayment(a.payment_hash,{timeout:3e4,interval:1e3}):a;eb(s.status),ey(s.payment_hash),eH(`${s.status} \xb7 ${ef} CKB \xb7 ${v(s.payment_hash)}`,"Success"===s.status?"success":"error");let i=(await eX()).find(e=>e.channel_id===(n?.channel_id??ew));eT(i?BigInt(i.local_balance):null)}catch(t){let e=t instanceof Error?t.message:"Unable to send the Testnet payment.";eb("Failed"),eI(e),eH(`Failed \xb7 ${e}`,"error")}finally{e_(null)}}},[eH,ew,eB,ef,Q,eh,eX]),eQ=(0,i.useCallback)(async()=>{await navigator.clipboard.writeText(eW.code),eA(!0),window.setTimeout(()=>eA(!1),1500)},[eW]),eZ=(0,i.useCallback)(async()=>{if(ee)try{await navigator.clipboard.writeText(ee),eO(!0),window.setTimeout(()=>eO(!1),1500)}catch{eI("Unable to copy the funding address.")}},[ee]),e0=(0,i.useCallback)(()=>{let e=W?B.current:_.current;e?.scrollIntoView({behavior:"smooth",block:"start"})},[W]),e1="running"===G&&Q>0,e2=eF?l.default.statusError:"Success"===ex?l.default.statusSuccess:null!==eB||eU?l.default.statusWaiting:l.default.statusInfo,e8=e1?ee?eM?eU?"Wait for ChannelReady before paying.":null:"Not enough Testnet CKB to open this channel.":"Wait for the funding address.":"Connect the browser node first.",e4=0===eh?"Wait for ChannelReady before sending a payment.":0===Q?"Reconnect the public peer first.":null,e3=e1&&!eM&&0===eo,e9=null===eB&&Q>0&&!!ee&&eM&&0===eo&&!eU&&0===eh,e6=null===eB&&eh>0&&Q>0,e5=Q>0?"Peer connected":"running"===G&&"prepare"===eB?"Connectingâ¦":"running"===G?"Not connected":"Node required",e7=Q>0?l.default.statusSuccess:"running"===G&&"prepare"===eB?l.default.statusWaiting:l.default.statusIdle,te=eh>0?l.default.statusSuccess:"Open failed"===er?l.default.statusError:eU?l.default.statusWaiting:"Not opened"===er?l.default.statusIdle:l.default.statusInfo,tt="Success"===ex?l.default.statusSuccess:"Failed"===ex?l.default.statusError:"Sending"===ex?l.default.statusWaiting:"Not sent"===ex?l.default.statusIdle:l.default.statusInfo;return(0,t.jsxs)("div",{className:l.default.shell,children:[(0,t.jsxs)("header",{className:l.default.tutorialToolbar,children:[(0,t.jsxs)("div",{className:l.default.tutorialSelect,ref:I,children:[(0,t.jsxs)("button",{"aria-expanded":L,"aria-haspopup":"menu",className:l.default.tutorialSelectButton,onClick:()=>H(e=>!e),type:"button",children:[(0,t.jsxs)("span",{children:["Tutorial: ",u[1].shortTitle]}),(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",className:l.default.selectChevron,height:18,src:"/icon-chevron-down.svg",width:18})]}),L&&(0,t.jsx)("div",{className:l.default.tutorialMenu,role:"menu",children:u.map((e,s)=>(0,t.jsxs)(a.default,{"aria-current":1===s?"page":void 0,className:1===s?l.default.tutorialMenuCurrent:void 0,href:e.href,onClick:()=>H(!1),role:"menuitem",children:[(0,t.jsx)("span",{children:e.shortTitle}),1===s&&(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",height:18,src:"/icon-checkmark.svg",width:18})]},e.href))})]}),(0,t.jsxs)("button",{className:l.default.runDemoAction,onClick:e0,type:"button",children:[W?"View code walkthrough":"Try live demo"," ",(0,t.jsx)("span",{"aria-hidden":"true",children:W?"â":"â"})]}),(0,t.jsx)("div",{className:l.default.toolbarActions,children:(0,t.jsxs)("a",{download:!0,href:"/downloads/fiber-c
386hannel-payment.zip",children:[(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",height:18,src:"/icon-download.svg",width:18}),"Download project"]})}),(0,t.jsx)("div",{className:l.default.horizontalProgress,"aria-hidden":"true",children:(0,t.jsx)("span",{className:l.default.progressFill,style:{transform:`scaleX(${E})`}})})]}),(0,t.jsxs)("section",{className:l.default.channelLiveDemo,ref:_,children:[(0,t.jsx)("div",{className:l.default.liveDemoHeading,children:(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Interactive tutorial · About 15 minutes"}),(0,t.jsx)("h1",{children:"Open a Fiber Channel and Send a Payment"}),(0,t.jsx)("p",{children:"Fund your browser node, open a real Fiber Testnet channel, and send 1 CKB to a public peer. Testnet confirmations may take longer than the active tutorial time."}),(0,t.jsxs)("p",{className:l.default.continuationNote,children:["Need background on browser nodes? Review"," ",(0,t.jsx)(a.default,{href:"/docs/build/connect-wasm-node",children:"Connect to Fiber with a WASM Node"}),"."]})]})}),(0,t.jsx)("div",{className:l.default.demoSteps,children:[["1","Prepare node","Start and connect."],["2","Fund address","Receive Testnet CKB."],["3","Open channel","Commit the peer minimum on-chain."],["4","Send payment","Send CKB to the peer."]].map(([e,n,a])=>(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:e}),(0,t.jsxs)("p",{children:[(0,t.jsxs)("strong",{children:[n,"."]})," ",a]})]},e))}),(0,t.jsx)("div",{className:l.default.channelDemoSurface,children:(0,t.jsxs)("div",{className:l.default.paymentPreviewStage,children:[(0,t.jsxs)("div",{className:l.default.paymentCard,children:[(0,t.jsxs)("div",{className:l.default.paymentStatusGrid,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Node"}),(0,t.jsxs)("strong",{children:[(0,t.jsx)("i",{className:`${l.default.statusDot} ${"running"===G?l.default.statusSuccess:l.default.statusWaiting}`}),w(G)]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Peer"}),(0,t.jsxs)("strong",{children:[(0,t.jsx)("i",{className:`${l.default.statusDot} ${e7}`}),e5]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Channel"}),(0,t.jsxs)("strong",{children:[(0,t.jsx)("i",{className:`${l.default.statusDot} ${te}`}),eh>0?"Channel ready":er]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Payment"}),(0,t.jsxs)("strong",{children:[(0,t.jsx)("i",{className:`${l.default.statusDot} ${tt}`}),"Success"===ex?"Payment successful":ex]})]})]}),(0,t.jsxs)("div",{className:l.default.paymentFlow,children:[(0,t.jsx)("div",{className:l.default.paymentFlowNumber,children:"1"}),(0,t.jsxs)("div",{className:l.default.paymentFlowBody,children:[(0,t.jsx)("strong",{children:"Prepare browser node"}),(0,t.jsx)("span",{children:"Start locally and connect over WSS. This does not move funds."})]}),(0,t.jsx)("button",{className:`${l.default.startButton} ${l.default.demoAction} ${!e1?l.default.demoPrimaryAction:""}`,disabled:null!==eB||e1,onClick:!1===eK?()=>window.location.reload():eG,type:"button",children:"prepare"===eB?"Preparingâ¦":!1===eK?"Reload to enable WASM":e1?"Node running":"Prepare browser node"})]}),(0,t.jsxs)("div",{className:l.default.paymentFlow,children:[(0,t.jsx)("div",{className:l.default.paymentFlowNumber,children:"2"}),(0,t.jsxs)("div",{className:l.default.paymentFlowBody,children:[(0,t.jsx)("strong",{children:"Fund the address"}),(0,t.jsxs)("div",{className:l.default.addressLine,children:[(0,t.jsx)("code",{title:ee,children:v(ee)}),(0,t.jsx)("button",{"aria-label":eE?"Funding address copied":"Copy funding address",className:`${l.default.demoAction} ${l.default.addressCopyButton}`,disabled:!ee,onClick:eZ,title:eE?"Copied":"Copy funding address",type:"button",children:(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",height:15,src:eE?"/icon-checkmark.svg":"/icon-copy.svg",width:15})})]}),(0,t.jsxs)("span",{children:["Balance: ",(0,t.jsx)("b",{children:g(en)})," · auto-checks every 5s"]})]}),(0,t.jsxs)("div",{className:l.default.fundingActions,children:[(0,t.jsx)(N,{id:"funding-action-requirement",label:"Get CKB",reason:ee?null:"Prepare the browser node to reveal its address.",children:ee?(0,t.jsx)("a",{className:`${l.default.faucetButton} ${l.default.demoAction} ${e3?l.default.demoPrimaryAction:""}`,href:"https://faucet.nervos.org",rel:"noreferrer",target:"_blank",children:"Get CKB â"}):(0,t.jsx)("button",{className:`${l.default.faucetButton} ${l.default.demoAction}`,disabled:!0,type:"button",children:"Get CKB â"})}),(0,t.jsxs)("button",{className:`${l.default.refreshButton} ${l.default.demoAction}`,disabled:!J||null!==eB,onClick:eV,type:"button",children:[(0,t.jsx)(C,{}),"refresh"===eB?"Refreshingâ¦":"Refresh"]})]})]}),(0,t.jsxs)("div",{className:l.default.paymentFlow,children:[(0,t.jsx)("div",{className:l.default.paymentFlowNumber,children:"3"}),(0,t.jsxs)("div",{className:l.default.paymentFlowBody,children:[(0,t.jsxs)("strong",{children:["Open a"," ",(0,t.jsx)(r.ExplainedTerm,{explanation:"The selected public peer requires at least 499 CKB to open a channel. Keep extra Testnet CKB available for the transaction fee.",id:"channel-minimum-funding",children:"499 CKB"})," ","channel"]}),(0,t.jsxs)("label",{children:[(0,t.jsx)("input",{"aria-label":"Channel funding amount in CKB",disabled:!J||null!==eB,inputMode:"decimal",onChange:e=>ei(e.target.value),value:es}),(0,t.jsx)("span",{children:"CKB"})]})]}),(0,t.jsx)(N,{id:"channel-action-requirement",label:"Open channel",reason:e8,children:(0,t.jsx)("button",{className:`${l.default.channelButton} ${l.default.demoAction} ${e9?l.default.demoPrimaryAction:""}`,disabled:null!==eB||0===Q||!ee||!eM||eo>0||eU||eh>0,onClick:eJ,children:"channel"===eB?"Openingâ¦":eh>0?"Channel ready":"Open channel"})})]}),ed.length>0&&(0,t.jsxs)("div",{className:l.default.channelTimeline,children:[(0,t.jsx)("span",{children:"Observed channel lifecycle"}),(0,t.jsxs)("div",{children:[ed.map((e,n)=>(0,t.jsxs)("span",{children:[n>0&&(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("b",{children:e})]},`${e}-${n}`)),eq&&(0,t.jsxs)("span",{"aria-label":`Waiting for ${eq}`,className:l.default.pendingChannelState,children:[(0,t.jsx)("i",{"aria-hidden":"true",children:"â"}),(0,t.jsx)("b",{children:eq})]})]})]}),(0,t.jsxs)("div",{className:l.default.paymentFlow,children:[(0,t.jsx)("div",{className:l.default.paymentFlowNumber,children:"4"}),(0,t.jsxs)("div",{className:l.default.paymentFlowBody,children:[(0,t.jsx)("strong",{children:"Send CKB to the peer"}),(0,t.jsxs)("label",{children:[(0,t.jsx)("input",{"aria-label":"Payment amount in CKB",disabled:0===eh||null!==eB,inputMode:"decimal",onChange:e=>em(e.target.value),value:ef}),(0,t.jsx)("span",{children:"CKB"})]})]}),(0,t.jsx)(N,{id:"payment-action-requirement",label:"Send payment",reason:e4,children:(0,t.jsx)("button",{className:`${l.default.paymentButton} ${l.default.demoAction} ${e6?l.default.demoPrimaryAction:""}`,disabled:null!==eB||0===eh||0===Q,onClick:eY,children:"payment"===eB?"Sendingâ¦":"Send payment"})})]}),ej&&(0,t.jsxs)("div",{className:l.default.paymentReceipt,children:[(0,t.jsx)("span",{children:"Payment receipt"}),(0,t.jsxs)("dl",{children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("dt",{children:"Status"}),(0,t.jsx)("dd",{children:ex})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("dt",{children:"Amount"}),(0,t.jsxs)("dd",{children:[eg," CKB"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("dt",{children:"Channel ID"}),(0,t.jsx)("dd",{title:ew,children:v(ew)})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("dt",{children:"Payment hash"}),(0,t.jsx)("dd",{title:ej,children:v(ej)})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("dt",{children:"Local balance"}),(0,t.jsxs)("dd",{children:[g(eC)," â ",g(ek)]})]})]})]}),eF&&(0,t.jsx)("div",{className:l.default.paymentError,role:"alert",children:eF})]}),(0,t.jsxs)("div",{className:l.default.eventPanel,children:[(0,t.jsxs)("div",{className:l.default.eventPanelHeader,children:[(0,t.jsx)("span",{children:"Runtime events and results"}),(0,t.jsx)("i",{className:`${l.default.liveDot} ${e2}`})]}),(0,t.jsxs)("div",{className:l.default.eventList,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:"NODE"}),(0,t.jsx)("code",{children:"state"}),(0,t.jsx)("span",{children:w(G)})]}),(0,t.jsxs)("div",{className:Q>0?l.default.eventConnected:void 0,children:[(0,t.jsx)("time",{children:"PEER"}),(0,t.jsx)("code",{children:"connection"}),(0,t.jsx)("span",{children:Q>0?"Connected":"Offline"})]}),0===ed.length&&0===eR.length&&!eF&&(0,t.jsx)("div",{className:l.default.eventEmpty,children:(0,t.jsx)("span",{children:"Channel and payment events will appear here."})}),ed.map((e,n)=>(0,t.jsxs)("div",{children:[(0,t.jsxs)("time",{children:["CH ",String(n+1).padStart(2,"0")]}),(0,t.jsx)("code",{children:"channel_state"}),(0,t.jsx)("span",{children:e})]},`${e}-event-${n}`)),eR.map((e,n)=>(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:e.at}),(0,t.jsx)("code",{children:"payment"}),(0,t.jsx)("span",{children:e.message})]},`${e.at}-event-${n}`)),eF&&(0,t.jsxs)("div",{className:l.default.eventError,children:[(0,t.jsx)("time",{children:"!"}),(0,t.jsx)("code",{children:"error"}),(0,t.jsx)("span",{children:eF})]})]})]})]})})]}),(0,t.jsx)("div",{"aria-hidden":"true",className:l.default.sectionDivider}),(0,t.jsxs)("header",{className:l.default.walkthroughHeader,ref:B,children:[(0,t.jsx)("span",{children:"Implementation walkthrough"}),(0,t.jsx)("h2",{children:"Behind the scenes"}),(0,t.jsx)("p",{children:"Follow the data and SDK calls that prepare the node, fund and open the channel, then confirm the payment."})]}),(0,t.jsxs)("section",{className:l.default.instructionWorkspace,children:[(0,t.jsxs)("div",{className:l.default.instructionGrid,children:[(0,t.jsxs)("div",{className:l.default.article,onClick:e=>{let t=e.target.closest("[data-tutorial-section]");t?.dataset.tutorialSection&&ez(t.dataset.tutorialSection)},ref:F,children:[(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"model",children:[(0,t.jsx)("div",{className:l.default.stepLabel,children:"Architecture"}),(0,t.jsx)("h2",{children:"Understand the channel payment flow"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"openChannel()"})," locks a chosen amount of Testnet CKB on-chain. Once the channel is ready, ",(0,t.jsx)("code",{children:"pay()"})," can make multiple off-chain payments without creating a new CKB transaction for every small amount."]}),(0,t.jsxs)("div",{className:l.default.flow,"aria-label":"Channel payment flow",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("b",{children:"Your browser node"}),(0,t.jsx)("span",{children:"Fiber WASM"})]}),(0,t.jsx)("span",{className:l.default.flowArrow,children:"â Fiber channel â"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("b",{children:"Public Fiber peer"}),(0,t.jsx)("span",{children:"Receives payment"})]})]}),(0,t.jsxs)("div",{className:`${l.default.note} ${l.default.noteInfo}`,children:[(0,t.jsx)("strong",{children:"Testnet state is real"}),(0,t.jsx)("p",{children:"Preparing the node starts it locally and connects it to the public peer. Opening the channel and sending payments are separate actions."})]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"app/pay/page.tsx · lines 39â48"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"identity",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"1"})," Prepare the node"]}),(0,t.jsx)("h2",{children:"Start the node and derive its funding address"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"start()"})," starts the browser node and reveals the Testnet address that must be funded. It calls ",(0,t.jsx)("code",{children:"startFiber()"}),", reads the node information, and uses ",(0,t.jsx)("code",{children:"scriptToAddress()"})," to convert ",(0,t.jsx)("code",{children:"default_funding_lock_script"})," into its Testnet address."]}),(0,t.jsxs)("p",{className:l.default.followupParagraph,children:["The demo combines startup and ",(0,t.jsx)("code",{children:"connectToRouter()"})," into one preparation action. The downloaded source keeps the helpers separate, so an application can expose more control when needed."]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"app/pay/page.tsx · lines 23â31"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"fund",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"2"})," Watch funding"]}),(0,t.jsx)("h2",{children:"Detect sufficient Testnet funding"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"queryCkbBalance()"})," calls CKB RPC ",(0,t.jsx)("code",{children:"get_cells_capacity"})," to measure the address's available capacity. ",(0,t.jsx)("code",{children:"watchCkbBalance()"})," repeats the check every five seconds while the page is visible, allowing the interface to unlock channel creation once the address can cover the funding amount."]}),(0,t.jsxs)("div",{className:`${l.default.note} ${l.default.noteWarning}`,children:[(0,t.jsx)("strong",{children:"Use Testnet funds only"}),(0,t.jsx)("p",{children:"The public peer accepts CKB channels at or above 499 CKB. Keep extra Testnet CKB available for the transaction fee."})]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"lib/balance.ts · lines 7â45"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"amounts",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"3"})," Format the amount"]}),(0,t.jsx)("h2",{children:"Convert CKB amounts without losing precision"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"ckbToHex()"})," converts the entered CKB amount into the exact hexadecimal shannon value expected by Fiber RPC. It uses ",(0,t.jsx)("code",{children:"BigInt"})," to avoid floating-point rounding across CKB's eight decimal places."]}),(0,t.jsxs)("div",{className:l.default.amountConversion,"aria-label":"499 CKB represented as shannons and hexadecimal",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"CKB amount"}),(0,t.jsx)("code",{children:"499 CKB"})]}),(0,t.jsx)("i",{"aria-hidden":"true",children:"â"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Smallest unit"}),(0,t.jsx)("code",{children:"49,900,000,000 shannons"})]}),(0,t.jsx)("i",{"aria-hidden":"true",children:"â"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Hexadecimal RPC value"}),(0,t.jsx)("code",{children:"0xb9e0ab300"})]})]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"lib/amounts.ts · lines 1â18"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"open",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"4"})," Commit funds"]}),(0,t.jsx)("h2",{children:"Open the channel"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"openCkbChannel()"})," begins channel negotiation with the connected public peer. It calls ",(0,t.jsx)("code",{children:"node.openChannel()"})," with the peer pubkey, the exact funding amount, and ",(0,t.jsx)("code",{children:"public: true"}),"."]}),(0,t.jsx)("p",{className:l.default.followupParagraph,children:"Its temporary channel ID confirms negotiation started; it does not mean the channel can carry a payment yet."}),(0,t.jsx)("small",{className:l.default.fileReference,children:"lib/channel.ts · lines 7â19"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"ready",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"5"})," Observe confirmation"]}),(0,t.jsx)("h2",{children:"Wait for ChannelReady"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"watchChannelStates()"})," keeps payment controls locked until the funding transaction and peer handshake complete. It polls ",(0,t.jsx)("code",{children:"listChannels()"})," and reports each new ",(0,t.jsx)("code",{children:"state_name"})," as the channel moves through the following states:"]}),(0,t.jsxs)("div",{className:l.default.checkList,children:[(0,t.jsx)("span",{children:"NegotiatingFunding â agree on channel parameters"}),(0,t.jsx)("span",{children:"CollaboratingFundingTx â build the funding transaction"}),(0,t.jsx)("span",{children:"AwaitingChannelReady â wait for confirmation
386 and both peers"}),(0,t.jsx)("span",{children:"ChannelReady â allow payments"})]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"lib/channel.ts · lines 21â40"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"pay",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"6"})," Pay and verify"]}),(0,t.jsx)("h2",{children:"Send and confirm the keysend payment"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"sendKeysend()"})," sends a direct payment without first requesting an invoice. Fiber calls this a keysend payment because it does not require the receiving peer to create an invoice. The function calls ",(0,t.jsx)("code",{children:"sendPayment()"})," with the recipient pubkey, amount, and ",(0,t.jsx)("code",{children:"keysend: true"}),"."]}),(0,t.jsxs)("p",{className:l.default.followupParagraph,children:["If the response is still in flight, ",(0,t.jsx)("code",{children:"waitForPayment()"})," waits for ",(0,t.jsx)("code",{children:"Success"})," or ",(0,t.jsx)("code",{children:"Failed"}),". The demo then shows the payment hash and the channel's local balance before and after."]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"lib/payment.ts · lines 4â26"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"react",children:[(0,t.jsxs)("div",{className:l.default.stepLabel,children:[(0,t.jsx)("span",{children:"7"})," React integration"]}),(0,t.jsx)("h2",{children:"Wire the complete flow into React"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"PayPage()"})," turns SDK state into a guided interface. It keeps the node in a ref, renders funding and channel state from React state, and gates each state-changing button on the prerequisite it needs."]}),(0,t.jsxs)("div",{className:`${l.default.note} ${l.default.noteWarning}`,children:[(0,t.jsx)("strong",{children:"Production requirements"}),(0,t.jsx)("p",{children:"A production product should also encrypt keys, support backup and recovery, surface fees and timeouts, and use server-side verification when payments unlock orders or access."})]}),(0,t.jsx)("small",{className:l.default.fileReference,children:"app/pay/page.tsx · lines 16â65"})]}),(0,t.jsxs)("section",{className:l.default.section,"data-tutorial-section":"download",children:[(0,t.jsx)("div",{className:l.default.stepLabel,children:"Optional local setup"}),(0,t.jsx)("h2",{children:"Run the complete project locally"}),(0,t.jsxs)("p",{children:["Select ",(0,t.jsx)("strong",{children:"Download project"})," in the top-right corner, or ",(0,t.jsx)("a",{className:l.default.inlineDownloadLink,download:!0,href:"/downloads/fiber-channel-payment.zip",children:"download here"}),". The archive contains the Next.js application, Fiber integration, required browser headers, and the interface shown here."]}),(0,t.jsxs)("div",{className:l.default.setupCodeBlock,children:[(0,t.jsx)("div",{className:l.default.setupCodeHeader,children:(0,t.jsx)("span",{children:"Terminal"})}),(0,t.jsx)("pre",{children:(0,t.jsx)("code",{children:`npm install 387npm run dev`})})]}),(0,t.jsxs)("p",{className:l.default.followupParagraph,children:["Then open ",(0,t.jsx)("code",{children:"http://localhost:3000/pay"})," in your browser."]})]})]}),(0,t.jsxs)("aside",{className:l.default.codePanel,"aria-label":"Tutorial project files",children:[(0,t.jsx)("div",{className:l.default.fileTabs,role:"tablist","aria-label":"Code files",children:h.map(e=>(0,t.jsx)("button",{"aria-selected":e.id===U,className:e.id===U?l.default.activeTab:"",onClick:()=>{q(e.id),X(null)},role:"tab",type:"button",children:e.label},e.id))}),(0,t.jsx)(m,{file:eW,focus:z}),(0,t.jsxs)("div",{className:l.default.codeMeta,children:[(0,t.jsxs)("span",{children:[eW.label," ",(0,t.jsx)("i",{children:eW.language})]}),(0,t.jsxs)("button",{"aria-label":e$?`${eW.label} copied`:`Copy ${eW.label}`,className:l.default.copyButton,onClick:eQ,type:"button",children:[(0,t.jsx)(n.default,{alt:"","aria-hidden":"true",height:18,src:e$?"/icon-checkmark.svg":"/icon-copy.svg",width:18}),e$?"Copied":"Copy file"]})]})]})]}),K&&(0,t.jsxs)("nav",{"aria-label":"Tutorial navigation",className:`${l.default.tutorialFooter} ${l.default.walkthroughFooter}`,children:[(0,t.jsxs)("button",{onClick:()=>R.push("/docs/build/connect-wasm-node"),type:"button",children:[(0,t.jsx)("span",{"aria-hidden":"true",children:"â"})," Previous"]}),(0,t.jsxs)("button",{onClick:()=>R.push("/docs/build/multi-hop-invoice"),type:"button",children:["Next ",(0,t.jsx)("span",{"aria-hidden":"true",children:"â"})]})]})]})]})}])},21478,e=>{"use strict";var t=e.i(99873),n=e.i(15300),a=e.i(29764),s=e.i(49083),i=e.i(84678);let r=[{id:"fiber",label:"lib/fiber.ts",language:"typescript",code:`import { 388 FiberBrowserNode, 389 RawKeyCredentialProvider, 390} from '@fiber-pay/sdk/browser'; 391 392export const bottle = { 393 pubkey: '0x02b6d4e3...302be71', 394 address: '/dns4/bottle.fiber.channel/tcp/443/wss/p2p/QmXen3e...XJ1Eo', 395}; 396 397export async function startRole( 398 role: 'sender' | 'receiver', 399 credential: RawKeyCredentialProvider, 400) { 401 const node = new FiberBrowserNode({ 402 network: 'testnet', 403 credential, 404 nodeConfig: { bootnodes: [], logLevel: 'info' }, 405 }); 406 await node.start(); 407 await node.connectPeer(bottle); 408 return node; 409}`},{id:"invoice",label:"lib/invoice.ts",language:"typescript",code:`import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 410import { ckbToHex } from './amounts'; 411 412export async function createReceiverInvoice( 413 node: FiberBrowserNode, 414 amount: string, 415) { 416 return node.newInvoice({ 417 amount: ckbToHex(amount), 418 currency: 'Fibt', 419 description: 'Browser multi-hop tutorial', 420 expiry: '0xe10', 421 allow_trampoline_routing: true, 422 }); 423} 424 425export async function readInvoiceStatus( 426 node: FiberBrowserNode, 427 paymentHash: \`0x\${string}\`, 428) { 429 return node.getInvoice({ payment_hash: paymentHash }); 430}`}
430,{id:"transfer",label:"lib/invoice-transfer.ts",language:"typescript",code:`export async function copyInvoice(invoice: string) { 431 await navigator.clipboard.writeText(invoice); 432} 433 434export async function pasteInvoice() { 435 const invoice = (await navigator.clipboard.readText()).trim(); 436 if (!invoice) throw new Error('The clipboard is empty'); 437 return invoice; 438} 439 440// The encoded invoice can also travel through a QR code, 441// chat message, email, or any other text transport.`},{id:"payment",label:"lib/payment.ts",language:"typescript",code:`import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 442import { bottle } from './fiber'; 443import { ckbToHex } from './amounts'; 444 445export async function payMultiHopInvoice( 446 node: FiberBrowserNode, 447 invoice: string, 448) { 449 const submitted = await node.sendPayment({ 450 invoice, 451 trampoline_hops: [bottle.pubkey], 452 max_fee_amount: ckbToHex('1'), 453 }); 454 455 if (submitted.status === 'Success' || submitted.status === 'Failed') { 456 return submitted; 457 } 458 return node.waitForPayment(submitted.payment_hash, { 459 timeout: 60_000, 460 interval: 1_000, 461 }); 462}`},{id:"app",label:"app/multi-hop/page.tsx",language:"tsx",code:`'use client'; 463 464import { useRef, useState } from 'react'; 465import { createReceiverInvoice } from '@/lib/invoice'; 466import { copyInvoice, pasteInvoice } from '@/lib/invoice-transfer'; 467import { payMultiHopInvoice } from '@/lib/payment'; 468 469export default function MultiHopPage() { 470 const sender = useRef(null); 471 const receiver = useRef(null); 472 const [generatedInvoice, setGeneratedInvoice] = useState(''); 473 const [senderInvoice, setSenderInvoice] = useState(''); 474 const [status, setStatus] = useState('Waiting'); 475 476 async function createInvoice() { 477 if (!receiver.current) return; 478 const result = await createReceiverInvoice(receiver.current, '1'); 479 setGeneratedInvoice(result.invoice_address); 480 await copyInvoice(result.invoice_address); 481 } 482 483 async function pasteForSender() { 484 setSenderInvoice(await pasteInvoice()); 485 } 486 487 async function pay() { 488 if (!sender.current || !senderInvoice) return; 489 const result = await payMultiHopInvoice(sender.current, senderInvoice); 490 setStatus(result.status); 491 } 492 493 return <main> 494 <button onClick={createInvoice}>Create invoice and copy</button> 495 <button onClick={pasteForSender}>Paste into Node A</button> 496 <button onClick={pay}>Pay invoice</button> 497 <p>{status}</p> 498 </main>; 499}`}],l={roles:{file:"fiber",start:11,end:24},invoice:{file:"invoice",start:4,end:14},status:{file:"invoice",start:16,end:22},transfer:{file:"transfer",start:1,end:13},route:{file:"payment",start:5,end:14},result:{file:"payment",start:16,end:22},react:{file:"app",start:8,end:30}};function o(e){return e.toLowerCase().includes("ready")}function c(e){let t=e.replace(/[^a-z0-9]/gi,"").toLowerCase();return"channelready"===t?"ready":"awaitingchannelready"===t?"confirming":"submitting"}function d({value:e,label:a="Copy"}){let[s,i]=(0,n.useState)(!1);return(0,t.jsx)("button",{disabled:!e,onClick:async()=>{await navigator.clipboard.writeText(e),i(!0),window.setTimeout(()=>i(!1),1500)},type:"button",children:s?"Copied":a})}function u({number:e,title:n,peerName:a,runtime:r,channelState:l,channelExists:c,progressStage:h,onConnectAndOpen:p}){let f=(r.balance??0n)>=BigInt((0,s.ckbToHex)("499")),m=r.peers.some(e=>(0,s.samePubkey)(e,s.bottlePeer.pubkey)),x=o(l),b=[{label:"Connect peer",complete:m,active:"connecting"===h},{label:"Submit funding",complete:c||"confirming"===h||x,active:"submitting"===h},{label:"Confirm on-chain",complete:x,active:"confirming"===h},{label:"Channel ready",complete:x,active:!1}];return(0,t.jsxs)("div",{className:i.default.multiHopNodeSetup,children:[(0,t.jsxs)("div",{className:i.default.paymentFlow,children:[(0,t.jsx)("div",{className:i.default.paymentFlowNumber,children:e}),(0,t.jsxs)("div",{children:[(0,t.jsx)("strong",{children:n}),(0,t.jsxs)("span",{children:["Start locally, then fund and open a channel to ",a,"."]})]}),(0,t.jsxs)("div",{className:i.default.compactActions,children:[(0,t.jsx)("button",{className:i.default.startButton,disabled:!!r.nodeInfo||!!r.busy,onClick:r.start,children:"start"===r.busy?"Startingâ¦":"Start"}),(0,t.jsx)("button",{className:i.default.connectButton,disabled:!r.no
499deInfo||c||"connecting"===h||"submitting"===h||"confirming"===h||"ready"===h||!f||!!r.busy,onClick:()=>void p(),children:"connecting"===h||r.busy.startsWith("connect")?"Connectingâ¦":"submitting"===h||"open channel"===r.busy?"Openingâ¦":"confirming"===h?"Confirmingâ¦":"ready"===h||x?"Channel ready":c?"Channel submitted":m?"Open 499 CKB":"Connect & open"})]})]}),(0,t.jsxs)("div",{className:i.default.multiHopFunding,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Funding address"}),(0,t.jsx)("code",{title:r.address,children:(0,s.shorten)(r.address,13,10)}),(0,t.jsx)(d,{value:r.address})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"On-chain balance"}),(0,t.jsxs)("strong",{children:[(0,s.hexToCkb)(r.balance)," CKB"]}),(0,t.jsx)("a",{href:"https://faucet.nervos.org",rel:"noreferrer",target:"_blank",children:"Faucet â"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Public channel"}),(0,t.jsx)("strong",{children:l}),(0,t.jsxs)("small",{children:["499"," CKB capacity"]})]})]}),(0,t.jsxs)("div",{className:i.default.channelConnectionProgress,children:[(0,t.jsx)("span",{children:"Channel progress"}),(0,t.jsx)("div",{children:b.map((e,n)=>(0,t.jsxs)("div",{className:`${e.complete?i.default.channelProgressComplete:""} ${e.active?i.default.channelProgressActive:""}`,children:[(0,t.jsx)("i",{children:e.complete?"â":n+1}),(0,t.jsx)("small",{children:e.label}),n<b.length-1&&(0,t.jsx)("b",{"aria-hidden":"true",children:"â"})]},e.label))}),"confirming"===h&&(0,t.jsx)("p",{children:"Funding submitted. The node may retry while CKB confirms and indexes the transaction. Keep this page open and do not submit again."})]}),r.error&&(0,t.jsx)("div",{className:i.default.nodeChannelError,children:r.error})]})}e.s(["FiberMultiHopInvoiceTutorial",0,function(){let e=(0,s.useFiberRoutingNode)("fiber-docs:multi-hop-sender-v1"),d=(0,s.useFiberRoutingNode)("fiber-docs:multi-hop-receiver-v1"),[h,p]=(0,n.useState)("1"),[f,m]=(0,n.useState)(""),[x,b]=(0,n.useState)(!1),[j,y]=(0,n.useState)(""),[g,v]=(0,n.useState)(""),[w,N]=(0,n.useState)(""),[C,S]=(0,n.useState)("None"),[k,T]=(0,n.use
499State)("Not sent"),[R,P]=(0,n.useState)([]),[B,_]=(0,n.useState)([]),[F,I]=(0,n.useState)(""),[$,A]=(0,n.useState)("idle"),[E,O]=(0,n.useState)("idle"),K=(0,n.useRef)(!1),D=(0,s.findReusableChannel)(e.channels,s.bottlePeer.pubkey),L=(0,s.findReusableChannel)(d.channels,s.bottlePeer.pubkey),H=!!(D&&o(D.state.state_name)),W=!!(L&&o(L.state.state_name));(0,n.useEffect)(()=>{D&&A(c(D.state.state_name))},[D,H]),(0,n.useEffect)(()=>{L&&O(c(L.state.state_name))},[L,W]);let M=(0,n.useCallback)(e=>{_(t=>[...t.slice(-8),e])},[]),U=(0,n.useCallback)(async(e,t,n)=>{let a=e.nodeRef.current;if(!a)return!1;let i=(await a.listChannels()).channels,r=(0,s.findReusableChannel)(i,t);if(r)return M(`${n} restored \xb7 ${r.state.state_name}`),await e.refresh(),c(r.state.state_name);let l=await e.run("open channel",e=>e.openChannel({pubkey:t,funding_amount:(0,s.ckbToHex)("499"),public:!0}));return l?(M(`${n} submitted \xb7 ${(0,s.shorten)(l.temporary_channel_id)}`),await e.refresh(),"confirming"):null},[M]),q=(0,n.useCallback)(async(e,t,n)=>{if(e.setError(""),n("connecting"),M(`${t} \xb7 connecting to Bottle`),!(e.peers.some(e=>(0,s.samePubkey)(e,s.bottlePeer.pubkey))||await e.connect(s.bottlePeer))){n("error"),M(`${t} \xb7 connection failed`);return}M(`${t} \xb7 Bottle connected`),n("submitting");let a=await U(e,s.bottlePeer.pubkey,t);if(!a){n("error"),M(`${t} \xb7 funding submission failed`);return}n(a),"ready"===a?M(`${t} \xb7 existing ready channel reused`):M(`${t} \xb7 existing or new channel is still progressing`)},[M,U]),z=(0,n.useCallback)(async()=>{b(!1),y("");let e=await d.run("create invoice",e=>e.newInvoice({amount:(0,s.ckbToHex)(h),currency:"Fibt",description:"Browser multi-hop tutorial",expiry:"0xe10",allow_trampoline_routing:!0}));if(e){m(e.invoice_address),N(e.invoice.data.payment_hash),S("Open"),v(""),I("");try{await navigator.clipboard.writeText(e.invoice_address),b(!0),window.setTimeout(()=>b(!1),1500),M(`Node C created and copied ${h} CKB invoice`)}catch{y("Invoice created, but automatic copying was blocked. Select the invoice text and copy it manually."),M(`Node C created ${h} CKB invoice`)}}},[M,h,d]);(0,n.useEffect)(()=>{if(!w||!d.nodeInfo)return;let e=async()=>{if(!K.current&&d.nodeRef.current){K.current=!0;try{let e=await d.nodeRef.current.getInvoice({payment_hash:w});S(t=>(t!==e.status&&M(`Invoice \xb7 ${e.status}`),e.status))}finally{K.current=!1}}};e();let t=window.setInterval(()=>void e(),2e3);return()=>window.clearInterval(t)},[M,w,d.nodeInfo,d.nodeRef]);let X=(0,n.useCallback)(async()=>{I("");try{let e=(await navigator.clipboard.readText()).trim();if(!e)throw Error("The clipboard is empty.");v(e),M("Invoice pasted into Node A")}catch(e){I(e instanceof Error?`${e.message} You can paste directly into the field.`:"Unable to read the clipboard. Paste directly into the field.")}},[M]),G=(0,n.useCallback)(async()=>{let t=g.trim();if(!t)return;T("Sending"),M("Node A submitted the pasted invoice");let n=await e.run("send payment",e=>e.sendPayment({invoice:t,trampoline_hops:[s.bottlePeer.pubkey],max_fee_amount:(0,s.ckbToHex)("1")}));if(!n)return void T("Failed");let a="Success"===n.status||"Failed"===n.status?n:await e.nodeRef.current.waitForPayment(n.payment_hash,{timeout:6e4,interval:1e3});T(a.status),P(a.routers?.[0]?.nodes.map(e=>e.pubkey)??[]),M(`${a.status} \xb7 ${(0,s.shorten)(a.payment_hash)}`)},[M,e,g]),V=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("header",{className:i.default.hero,"data-tutorial-section":"intro",children:[(0,t.jsxs)("div",{className:i.default.eyebrow,children:[(0,t.jsx)("span",{children:"Routing"}),(0,t.jsx)("span",{className:i.default.eyebrowRule}),(0,t.jsx)("span",{children:"25 minute tutorial"})]}),(0,t.jsx)("h1",{children:"Send a Multi-Hop Invoice Payment"}),(0,t.jsx)("p",{className:i.default.lead,children:"Run sender and receiver Fiber nodes in one React page, copy the Invoice created by Node C, and paste it into Node A before paying through one public Fiber node."}),(0,t.jsxs)("div",{className:i.default.heroMeta,children:[(0,t.jsx)("span",{children:"Invoice"}),(0,t.jsx)("span",{children:"Multi-hop"}),(0,t.jsx)("span",{children:"Copy and paste"})]})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"roles",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"1"})," Start two roles"]}),(0,t.jsx)("h2",{children:"Keep Node A and Node C as separate identities"}),(0,t.jsx)("p",{children:"The page starts two independent Fiber WASM nodes with different local keys and storage identifiers. Node A and Node C each connect to Bottle, but they open separate public channels and do not share private node state."}),(0,t.jsxs)("div",{className:i.default.routeDiagram,children:[(0,t.jsx)("b",{children:"Node A"}),(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("span",{children:"Bottle · Public node"}),(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("b",{children:"Node C"})]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/fiber.ts · lines 11â24"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"invoice",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"2"})," Create the invoice"]}),(0,t.jsx)("h2",{children:"Let the receiver own the payment hash and preimage"}),(0,t.jsxs)("p",{children:["After Node C has a ready public channel, call ",(0,t.jsx)("code",{children:"newInvoice()"}),". Node C stores the invoice state and the secret required to settle the final TLC. The encoded Invoice is safe to hand to the payer."]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/invoice.ts · lines 4â14"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"transfer",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"3"})," Transfer the invoice"]}),(0,t.jsx)("h2",{children:"Copy from Node C, then paste into Node A"}),(0,t.jsx)("p",{children:"Copying makes the boundary explicit: an Invoice is portable text, not a privileged server message. The same value could be delivered by QR code, chat, email, or another application without changing the Fiber payment."}),(0,t.jsxs)("div",{className:i.default.note,children:[(0,t.jsx)("strong",{children:"No coordination backend"}),(0,t.jsx)("p",{children:"This tutorial keeps both nodes on one page for convenience, but transfers only the encoded Invoice through the clipboard. Production applications may choose any transport appropriate for their users."})]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/invoice-transfer.t
499s · lines 1â13"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"status",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"4"})," Observe invoice state"]}),(0,t.jsx)("h2",{children:"Poll the node that created the invoice"}),(0,t.jsxs)("p",{children:["Node C calls ",(0,t.jsx)("code",{children:"getInvoice()"})," and displays",(0,t.jsx)("code",{children:" Open â Received â Paid"}),". Cancelled and Expired are terminal outcomes too."]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/invoice.ts · lines 16â22"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"route",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"5"})," Route the payment"]}),(0,t.jsx)("h2",{children:"Use Bottle as the single public intermediary"}),(0,t.jsx)("p",{children:"Node A submits the pasted Invoice with Bottle as its trampoline hop. Bottle receives through the AâBottle channel and forwards through the separate BottleâC channel. A fee cap prevents an unexpectedly expensive route."}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/payment.ts · lines 5â14"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"result",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"6"})," Confirm both ends"]}),(0,t.jsx)("h2",{children:"Sender success and a paid Invoice complete the flow"}),(0,t.jsxs)("p",{children:["Wait for Node A's payment to become terminal, then confirm that Node C reports ",(0,t.jsx)("code",{children:"Paid"}),". The payment result also exposes the route that carried the transfer."]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/payment.ts · lines 16â22"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"react",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"7"})," Wire React"]}),(0,t.jsx)("h2",{children:"Keep generated and pasted values separate"}),(0,t.jsxs)("p",{children:["Node C owns ",(0,t.jsx)("code",{children:"generatedInvoice"}),"; Node A pays",(0,t.jsx)("code",{children:" senderInvoice"}),". Keeping two state values prevents the UI from silently transferring the Invoice behind the user's back."]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"app/multi-hop/page.tsx · lines 8â30"})]})]}),J=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:i.default.panelHeader,children:[(0,t.jsxs)("span",{children:[(0,t.jsx)("i",{className:i.default.liveDot})," Manual Invoice transfer"]}),(0,t.jsx)("button",{className:i.default.headerAction,disabled:!e.nodeInfo&&!d.nodeInfo,onClick:()=>{e.refresh(),d.refresh()},children:"Refresh both"})]}),(0,t.jsxs)("div",{className:`${i.default.previewStage} ${i.default.paymentPreviewStage}`,children:[(0,t.jsxs)("div",{className:`${i.default.paymentCard} ${i.default.multiHopPaymentCard}`,children:[(0,t.jsxs)("div",{className:i.default.routeStatusGrid,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Node A"}),(0,t.jsx)("strong",{children:"running"===e.nodeState?"Running":"Stopped"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Node C"}),(0,t.jsx)("strong",{children:"running"===d.nodeState?"Running":"Stopped"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Invoice"}),(0,t.jsx)("strong",{children:C})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Payment"}),(0,t.jsx)("strong",{children:k})]})]}),(0,t.jsxs)("div",{className:i.default.routeDiagram,"aria-label":"Multi-hop route",children:[(0,t.jsx)("b",{children:"Node A"}),(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("span",{children:"Bottle · Public node"}),(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("b",{children:"Node C"})]}),(0,t.jsx)(u,{channelExists:!!D,channelState:D?.state.state_name??"Not opened",number:"A",onConnectAndOpen:()=>q(e,"Node A channel",A),peerName:"Bottle",runtime:e,progressStage:$,title:"Prepare sender Node A"}),(0,t.jsx)(u,{channelExists:!!L,channelState:L?.state.state_name??"Not opened",number:"C",onConnectAndOpe
499n:()=>q(d,"Node C channel",O),peerName:"Bottle",runtime:d,progressStage:E,title:"Prepare receiver Node C"}),(0,t.jsxs)("div",{className:i.default.invoiceTransfer,children:[(0,t.jsxs)("div",{className:i.default.invoiceSide,children:[(0,t.jsx)("span",{children:"Node C · Create"}),(0,t.jsxs)("label",{children:[(0,t.jsx)("input",{"aria-label":"Invoice amount in CKB",disabled:!W||!!d.busy,inputMode:"decimal",onChange:e=>p(e.target.value),value:h}),(0,t.jsx)("i",{children:"CKB"})]}),(0,t.jsx)("button",{disabled:!W||!!d.busy,onClick:()=>void z(),children:"create invoice"===d.busy?"Creating and copyingâ¦":x?"Invoice copied":"Create invoice and copy"}),(0,t.jsx)("textarea",{"aria-label":"Invoice generated by Node C",readOnly:!0,placeholder:"Node C invoice appears here",value:f}),j&&(0,t.jsx)("small",{children:j})]}),(0,t.jsxs)("div",{className:i.default.invoiceTransferArrow,"aria-hidden":"true",children:[(0,t.jsx)("span",{children:"Copy"}),(0,t.jsx)("b",{children:"â"}),(0,t.jsx)("span",{children:"Paste"})]}),(0,t.jsxs)("div",{className:i.default.invoiceSide,children:[(0,t.jsx)("span",{children:"Node A · Pay"}),(0,t.jsx)("textarea",{"aria-label":"Invoice pasted into Node A",onChange:e=>v(e.target.value),placeholder:"Paste the Invoice here",value:g}),(0,t.jsx)("button",{disabled:!f,onClick:()=>void X(),children:"Paste from clipboard"}),(0,t.jsx)("button",{className:i.default.paymentButton,disabled:!H||!g.trim()||!!e.busy,onClick:()=>void G(),children:"send payment"===e.busy?"Sendingâ¦":"Pay pasted invoice"}),F&&(0,t.jsx)("small",{children:F})]})]})]}),(0,t.jsxs)("div",{className:i.default.eventPanel,children:[(0,t.jsxs)("div",{className:i.default.eventPanelHeader,children:[(0,t.jsx)("span",{children:"Route events and results"}),(0,t.jsx)("i",{className:i.default.liveDot})]}),(0,t.jsxs)("div",{className:i.default.eventList,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:"A"}),(0,t.jsx)("code",{children:"sender_channel"}),(0,t.jsx)("span",{children:D?.state.state_name??"Not opened"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:"C"}),(0,t.jsx)("code",{children:"receiver_channel"}),(0,t.jsx)("span",{children:L?.state.state_name??"Not opened"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:"INV"}),(0,t.jsx)("code",{children:"invoice_status"}),(0,t.jsx)("span",{children:C})]}),R.map((e,n)=>(0,t.jsxs)("div",{children:[(0,t.jsxs)("time",{children:["H",n+1]}),(0,t.jsx)("code",{children:"route_hop"}),(0,t.jsx)("span",{children:(0,s.shorten)(e)})]},`${e}-${n}`)),B.map((e,n)=>(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:String(n+1).padStart(2,"0")}),(0,t.jsx)("code",{children:"event"}),(0,t.jsx)("span",{children:e})]},`${e}-${n}`))]})]})]})]});return(0,t.jsx)(a.RoutingTutorialFrame,{article:V,codeFiles:r,currentTutorialIndex:2,defaultFile:"invoice",demoDescription:"Run two independent browser nodes, manually transfer the Invoice, and pay through one public Bottle node.",demoTitle:"Run the Multi-Hop Demo",downloadHref:"/downloads/fiber-multi-hop-invoice.zip",liveDemo:J,nextHref:"/docs/build/unidirectional-channel",previousHref:"/docs/build/open-channel-payment",sectionCode:l})}])},41633,e=>{"use strict";var t=e.i(99873),n=e.i(15300),a=e.i(29764),s=e.i(49083),i=e.i(84678);let r=[{id:"fiber",label:"lib/fiber.ts",language:"typescript",code:`import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 500 501export const bottle = { 502 pubkey: '0x02b6d4e3...302be71' as const, 503 address: '/dns4/bottle.fiber.channel/tcp/443/wss/p2p/QmXen3e...XJ1Eo', 504}; 505 506export async function connectBottle(node: FiberBrowserNode) { 507 await node.connectPeer(bottle); 508}`},{id:"channel",label:"lib/channel.ts",language:"typescript",code:`import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 509import { ckbToHex } from './amounts'; 510 511export async function openOneWayChannel( 512 node: FiberBrowserNode, 513 acceptorPubkey: \`0x\${string}\`, 514) { 515 return node.openChannel({ 516 pubkey: acceptorPubkey, 517 funding_amount: ckbToHex('499'), 518 public: false, 519 one_way: true, 520 }); 521} 522
523export async function readOneWayChannel( 524 node: FiberBrowserNode, 525 acceptorPubkey: string, 526) { 527 const { channels } = await node.listChannels(); 528 return channels.find( 529 (channel) => 530 channel.pubkey === acceptorPubkey && channel.is_one_way, 531 ); 532}`},{id:"payment",label:"lib/payment.ts",language:"typescript",code:`import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 533import { ckbToHex } from './amounts'; 534 535export async function sendOneWayPayment( 536 node: FiberBrowserNode, 537 acceptorPubkey: \`0x\${string}\`, 538 amount: string, 539) { 540 const submitted = await node.sendPayment({ 541 target_pubkey: acceptorPubkey, 542 amount: ckbToHex(amount), 543 keysend: true, 544 }); 545 546 if (submitted.status === 'Success' || submitted.status === 'Failed') { 547 return submitted; 548 } 549 return node.waitForPayment(submitted.payment_hash, { 550 timeout: 60_000, 551 interval: 1_000, 552 }); 553}`},{id:"app",label:"app/page.tsx",language:"tsx",code:`'use client'; 554 555import { useRef, useState } from 'react'; 556import type { FiberBrowserNode } from '@fiber-pay/sdk/browser'; 557import { bottle, connectBottle } from '@/lib/fiber'; 558import { openOneWayChannel, readOneWayChannel } from '@/lib/channel'; 559import { sendOneWayPayment } from '@/lib/payment'; 560 561export default function OneWayChannelPage() { 562 const node = useRef<FiberBrowserNode | null>(null); 563 const [status, setStatus] = useState('Not opened'); 564 565 async function open() { 566 if (!node.current) return; 567 await connectBottle(node.current); 568 await openOneWayChannel(node.current, bottle.pubkey); 569 setStatus('Opening â wait for ChannelReady'); 570 } 571 572 async function pay() { 573 if (!node.current) return; 574 const channel = await readOneWayChannel(node.current, bottle.pubkey); 575 if (channel?.state.state_name !== 'CHANNEL_READY') return; 576 const result = await sendOneWayPayment(node.current, bottle.pubkey, '1'); 577 setStatus(result.status); 578 } 579 580 return <main> 581 <p>{status}</p> 582 <button onClick={open}>Open one-way channel</button> 583 <button onClick={pay}>Send 1 CKB</button> 584 </main>; 585}`}],l={model:{file:"channel",start:4,end:14},connect:{file:"fiber",start:3,end:10},inspect:{file:"channel",start:16,end:25},payment:{file:"payment",start:4,end:22},react:{file:"app",start:9,end:29}};function o(e){return e.replace(/[^a-z0-9]/gi,"").toLowerCase()}function c(e){return!!(e&&"channelready"===o(e.state.state_name))}function d(e){if(!e)return"idle";let t=o(e.state.state_name);return"channelready"===t?"ready":t.includes("failed")||t.includes("aborted")?"error":"awaitingchannelready"===t?"confirming":"submitting"}function u(e){let t=e.filter(e=>(0,s.samePubkey)(e.pubkey,s.bottlePeer.pubkey)&&e.is_one_way&&(0,s.isReusableChannel)(e));return t.find(e=>c(e))??t[0]}function h({stage:e}){let n="connecting"===e?0:"submitting"===e?1:"confirming"===e?2:"ready"===e?3:-1,a=["Connect peer","Submit funding","Confirm on-chain","Channel ready"];return(0,t.jsxs)("div",{className:i.default.channelConnectionProgress,children:[(0,t.jsx)("span",{children:"One-way channel progress"}),(0,t.jsx)("div",{children:a.map((s,r)=>{let l="ready"===e||r<n;return(0,t.jsxs)("div",{className:l?i.default.channelProgressComplete:r===n&&"error"!==e?i.default.channelProgressActive:void 0,children:[(0,t.jsx)("i",{children:l?"â":r+1}),(0,t.jsx)("small",{children:s}),r<a.length-1&&(0,t.jsx)("b",{children:"â"})]},s)})}),"confirming"===e&&(0,t.jsx)("p",{children:"Funding is on-chain. The page keeps checking until the channel is ready."})]})}e.s(["FiberUnidirectionalChannelTutorial",0,function(){let e=(0,s.useFiberRoutingNode)("fiber-docs:unidirectional-channel-v1"),[o,p]=(0,n.useState)("1"),[f,m]=(0,n.useState)("Not sent"),[x,b]=(0,n.useState)([]),[j,y]=(0,n.useState)("idle"),[g,v]=(0,n.useState)(null),[w,N]=(0,n.useState)(null),C=(0,n.useMemo)(()=>u(e.channels),[e.channels]),S=c(C),k=e.peers.some(e=>(0,s.samePubkey)(e,s.bottlePeer.pubkey)),T=(e.balance??0n)>=BigInt((0,s.ckbToHex)("499"));(0,n.useEffect)(()=>{C&&y(d(C))},[C]);let R=(0,n.useCallback)(e=>{b(t=>[...t.slice(-8),e])},[]),P=(0,n.useCallback)(async()=>{let t=e.nodeRef.current;if(!t)return;if(e.setError(""),y("connecting"),R("Connecting the payer to Bottle over WSS"),!(k||await e.connect(s.bottlePeer))){y("error"),R("Peer connection failed");return}R("Bottle connected");let n=u((await t.listChannels()).channels);if(n){y(d(n)),R(`Existing one-way channel restored \xb7 ${n.state.state_name}`),await e.refresh();return}y("submitting");let a=await e.run("open one-way channel",e=>e.openChannel({pubkey:s.bottlePeer.pubkey,funding_amount:(0,s.ckbToHex)("499"),public:!1,one_way:!0}));if(!a){y("error"),R("One-way channel submission failed");return}y("confirming"),R(`Private one-way channel submitted \xb7 ${(0,s.shorten)(a.temporary_channel_id)}`),await e.refresh()},[R,k,e]),B=(0,n.useCallback)(async()=>
585{if(!e.nodeRef.current||!C)return;v(BigInt(C.local_balance)),N(null),m("Sending"),R(`Sending ${o} CKB from payer to acceptor`);let t=await e.run("send one-way payment",e=>e.sendPayment({target_pubkey:s.bottlePeer.pubkey,amount:(0,s.ckbToHex)(o),keysend:!0}));if(!t)return void m("Failed");let n="Success"===t.status||"Failed"===t.status?t:await e.nodeRef.current.waitForPayment(t.payment_hash,{timeout:6e4,interval:1e3});m(n.status),R(`${n.status} \xb7 ${(0,s.shorten)(n.payment_hash)}`),await e.refresh();let a=u((await e.nodeRef.current.listChannels()).channels);a&&N(BigInt(a.local_balance))},[R,o,C,e]),_=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("header",{className:i.default.hero,"data-tutorial-section":"intro",children:[(0,t.jsxs)("div",{className:i.default.eyebrow,children:[(0,t.jsx)("span",{children:"Channel model"}),(0,t.jsx)("span",{className:i.default.eyebrowRule}),(0,t.jsx)("span",{children:"20 minute tutorial"})]}),(0,t.jsx)("h1",{children:"Open a Unidirectional Fiber Channel"}),(0,t.jsx)("p",{className:i.default.lead,children:"Fund a private one-way channel, verify the payer and acceptor roles, and send a real Testnet payment in the permitted direction."}),(0,t.jsxs)("div",{className:i.default.heroMeta,children:[(0,t.jsx)("span",{children:"One-way"}),(0,t.jsx)("span",{children:"Private channel"}),(0,t.jsx)("span",{children:"Keysend"})]})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"model",children:[(0,t.jsx)("div",{className:i.default.stepLabel,children:"The model"}),(0,t.jsx)("h2",{children:"Only the initiator can send"}),(0,t.jsx)("p",{children:"The browser node opens and funds the channel, so it becomes the payer. Bottle accepts the channel and becomes the receiver. During the channel lifetime, payments can move from the initiator to the acceptor, but not back."}),(0,t.jsxs)("div",{className:i.default.routeDiagram,children:[(0,t.jsx)("b",{children:"Browser payer"}),(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("span",{children:"Private one-way channel"}),(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("b",{children:"Bottle acceptor"})]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/channel.ts · lines 4â14"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"connect",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"1"})," Connect the acceptor"]}),(0,t.jsx)("h2",{children:"A public node can accept a private channel"}),(0,t.jsx)("p",{children:"Connect to Bottle over WSS before requesting a channel. Bottle is a public Fiber node, but this specific channel remains private and never appears in the public routing graph."}),(0,t.jsxs)("div",{className:i.default.note,children:[(0,t.jsx)("strong",{children:"Public peer â public channel"}),(0,t.jsxs)("p",{children:["One-way channels must use ",(0,t.jsx)("code",{children:"public: false"}),". Combining",(0,t.jsx)("code",{children:" one_way: true"})," with a public channel is rejected."]})]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/fiber.ts · lines 3â10"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"inspect",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"2"})," Open and inspect"]}),(0,t.jsx)("h2",{children:"Set one_way when funding the channel"}),(0,t.jsxs)("p",{children:["Submit 499 Testnet CKB with ",(0,t.jsx)("code",{children:"one_way: true"})," and wait for",(0,t.jsx)("code",{children:" ChannelReady"}),". The returned channel confirms",(0,t.jsx)("code",{children:" is_one_way: true"}),", ",(0,t.jsx)("code",{children:"is_public: false"}),", and",(0,t.jsx)("code",{children:" is_acceptor: false"})," for this browser node."]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/channel.ts · lines 16â25"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"payment",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"3"})," Pay forward"]}),(0,t.jsx)("h2",{children:"Send normally in the allowed direction"}),(0,t.jsxs)("p",{children:["Once ready, the payer uses the ordinary ",(0,t.jsx)("code",{children:"sendPayment()"})," API. The routing engine recognizes the channel direction, sends directly to Bottle, and rejects any attempt by the acceptor to pay back through it."]}),(0,t.jsx)("small",{className:i.default.fileReference,children:"lib/payment.ts · lines 4â22"})]}),(0,t.jsxs)("section",{className:i.default.section,"data-tutorial-section":"react",children:[(0,t.jsxs)("div",{className:i.default.stepLabel,children:[(0,t.jsx)("span",{children:"4"})," Show the result"]}),(0,t.jsx)("h2",{children:"Make the role and balance movement visible"}),(0,t.jsx)("p",{children:"The React page displays the channel flags alongside local and remote balances. After a successful payment, local payer balance decreases and remote acceptor balance increases without another on-chain transaction."}),(0,t.jsx)("small",{className:i.default.fileReference,children:"app/page.tsx · lines 9â29"})]})]}),F=null!==g&&null!==w?w-g:null,I=S?"Channel ready":C?"confirming"===j?"Confirmingâ¦":"Openingâ¦":"open one-way channel"===e.busy?"Openingâ¦":e.busy===`connect:${s.bottlePeer.name}`?"Connectingâ¦":k?"Open 499 CKB":"Connect & open",$=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:i.default.panelHeader,children:[(0,t.jsxs)("span",{children:[(0,t.jsx)("i",{className:i.default.liveDot})," One-way Testnet flow"]}),(0,t.jsx)("button",{className:i.default.headerAction,disabled:!e.nodeInfo,onClick:()=>void e.refresh(),children:"Refresh"})]}),(0,t.jsxs)("div",{className:`${i.default.previewStage} ${i.default.paymentPreviewStage}`,children:[(0,t.jsxs)("div",{className:i.default.paymentCard,children:[(0,t.jsxs)("div",{className:i.default.routeStatusGrid,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Node"}),(0,t.jsx)("strong",{children:"running"===e.nodeState?"Running":"Stopped"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Peer"}),(0,t.jsx)("strong",{children:k?"Connected":"Offline"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Channel"}),(0,t.jsx)("strong",{children:S?"Ready":C?.state.state_name??"Not opened"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Payment"}),(0,t.jsx)("strong",{children:f})]})]}),(0,t.jsxs)("div",{className:i.default.routeDiagram,children:[(0,t.jsx)("b",{children:"Browser payer"}),(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("span",{children:"One-way private channel"}),(0,t.jsx)("i",{children:"â"}),(0,t.jsx)("b",{children:"Bottle acceptor"})]}),(0,t.jsxs)("div",{className:i.default.paymentFlow,children:[(0,t.jsx)("div",{className:i.default.paymentFlowNumber,children:"1"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("strong",{children:"Start and fund the browser node"}),(0,t.jsxs)("span",{children:["Balance: ",(0,s.hexToCkb)(e.balance)," CKB"]})]}),(0,t.jsxs)("div",{className:i.default.compactActions,children:[(0,t.jsx)("button",{className:i.default.startButton,disabled:!!e.nodeInfo||!!e.busy,onClick:e.start,children:"start"===e.busy?"Startingâ¦":"Start"}),(0,t.jsx)("a",{className:i.default.faucetButton,href:"https://faucet.nervos.org",rel:"noreferrer",target:"_blank",children:"Faucet â"})]})]}),e.address&&(0,t.jsxs)("div",{className:i.default.rebalanceAddress,children:[(0,t.jsx)("code",{title:e.address,children:(0,s.shorten)(e.address,16,12)}),(0,t.jsx)("button",{onClick:()=>void navigator.clipboard.writeText(e.address),children:"Copy"})]}),(0,t.jsxs)("div",{className:i.default.paymentFlow,children:[(0,t.jsx)("div",{className:i.default.paymentFlowNumber,children:"2"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("strong",{children:"Connect and open a one-way channel"}),(0,t.jsx)("span",{children:"499 CKB · private · initiator â acceptor"})]}),(0,t.jsx)("button",{className:i.default.paymentButton,disabled:!e.nodeInfo||!T||!!C||!!e.busy,onClick:()=>void P(),children:I})]}),(0,t.jsx)(h,{stage:j}),(0,t.jsxs)("div",{className:i.default.oneWayChannelFacts,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Direction"}),(0,t.jsx)("strong",{children:"Initiator â Acceptor"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"is_one_way"}),(0,t.jsx)("strong",{children:C?String(C.is_one_way):"â"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"is_public"}),(0,t.jsx)("strong",{children:C?String(C.is_public):"â"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Your role"}),(0,t.jsx)("strong",{children:C?C.is_acceptor?"Acceptor":"Initiator":"â"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Local balance"}),(0,t.jsxs)("strong",{children:[(0,s.hexToCkb)(C?.local_balance)," CKB"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Remote balance"}),(0,t.jsxs)("strong",{children:[(0,s.hexToCkb)(C?.remote_balance)," CKB"]})]})]}),(0,t.jsxs)("div",{className:i.default.paymentFlow,children:[(0,t.jsx)("div",{className:i.default.paymentFlowNumber,children:"3"}),(0,t.jsxs)("div",{className:i.default.paymentFlowBody,children:[(0,t.jsx)("strong",{children:"Send toward the acceptor"}),(0,t.jsxs)("label",{children:[(0,t.jsx)("input",{"aria-label":"One-way payment amount in CKB",disabled:!S||!!e.busy,inputMode:"decimal",onChange:e=>p(e.target.value),value:o}),(0,t.jsx)("span",{children:"CKB"})]})]}),(0,t.jsx)("button",{className:i.default.paymentButton,disabled:!S||!!e.busy,onClick:()=>void B(),children:"send one-way payment"===e.busy?"Sendingâ¦":"Send payment"})]}),null!==g&&(0,t.jsxs)("div",{className:i.default.balanceComparison,children:[(0,t.jsx)("span",{children:"Payer local balance"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("b",{children:"Before â after"}),(0,t.jsxs)("code",{children:[(0,s.hexToCkb)(g)," â ",(0,s.hexToCkb)(w)]}),(0,t.jsx)("i",{children:null===F?"Pending":`${F>0n?"+":""}${(0,s.hexToCkb)(F)} CKB`})]})]}),e.error&&(0,t.jsx)("div",{className:i.default.paymentError,children:e.error})]}),(0,t.jsxs)("div",{className:i.default.eventPanel,children:[(0,t.jsxs)("div",{className:i.default.eventPanelHeader,children:[(0,t.jsx)("span",{children:"Channel events and results"}),(0,t.jsx)("i",{className:i.default.liveDot})]}),(0,t.jsx)("div",{className:i.default.eventList,children:x.map((e,n)=>(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:String(n+1).padStart(2,"0")}),(0,t.jsx)("code",{children:"one_way"}),(0,t.jsx)("span",{children:e})]},`${e}-${n}`))})]})]})]});return(0,t.jsx)(a.RoutingTutorialFrame,{article:_,codeFiles:r,currentTutorialIndex:3,defaultFile:"channel",demoDescription:"Open a private one-way channel to Bottle, inspect its roles, and send a real Testnet payment forward.",demoTitle:"Run the Unidirectional Channel Demo",downloadHref:"/downloads/fiber-unidirectional-channel.zip",liveDemo:$,nextHref:"/docs/build/hold-invoice",previousHref:"/docs/build/multi-hop-invoice",sectionCode:l})}])},63968,e=>{"use strict";var t=e.i(99873),n=e.i(15300),a=e.i(29764),s=e.i(49083),i=e.i(80296),r=e.i(84678);let l={code_hash:"0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a",hash_type:"type",args:"0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b"},o=e=>{if(!/^\d+(\.\d{1,8})?$/.test(e.trim()))throw Error("Enter a valid RUSD amount.");let[t,n=""]=e.trim().split(".");return`0x${(100000000n*BigInt(t)+BigInt(n.padEnd(8,"0"))).toString(16)}`},c=e=>{if(null==e)return"â";let t=(e%100000000n).toString().padStart(8,"0").replace(/0+$/,"");return`${e/100000000n}${t?`.${t}`:""}`};async function d(e){let t=await fetch("https://testnet.ckbapp.dev/",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:1,method:"get_cells",params:[{script:e,script_type:"lock",filter:{script:l}},"asc","0x64"]})}),n=await t.json();if(!n.result?.objects)throw Error(n.error?.message??"Unable to query RUSD balance.");return n.result.objects.reduce((e,t)=>
585{let n;return e+((n=(t.output_data.replace(/^0x/,"").slice(0,32).match(/../g)??[]).reverse().join(""))?BigInt(`0x${n}`):0n)},0n)}let u=[{id:"asset",label:"lib/rusd.ts",language:"typescript",code:`export const rusdTypeScript = { 586 code_hash: '0x1142755a...677d21a', 587 hash_type: 'type', 588 args: '0x878fcc6f...526439b', 589}; 590 591export async function queryRusdBalance(lockScript) { 592 const cells = await indexer.getCells({ 593 script: lockScript, script_type: 'lock', 594 filter: { script: rusdTypeScript }, 595 }); 596 return cells.reduce((sum, cell) => sum + readLeU128(cell.output_data), 0n); 597}`},{id:"channel",label:"lib/channel.ts",language:"typescript",code:`export async function openRusdChannel(node) { 598 await node.connectPeer(bottle); 599 return node.openChannel({ 600 pubkey: bottle.pubkey, 601 funding_amount: rusdToHex('20'), 602 funding_udt_type_script: rusdTypeScript, 603 public: true, 604 }); 605}`},{id:"payment",label:"lib/payment.ts",language:"typescript",code:`export async function sendRusd(node, amount) { 606 const submitted = await node.sendPayment({ 607 target_pubkey: bottle.pubkey, 608 amount: rusdToHex(amount), 609 udt_type_script: rusdTypeScript, 610 keysend: true, 611 }); 612 if (submitted.status === 'Success' || submitted.status === 'Failed') return submitted; 613 return node.waitForPayment(submitted.payment_hash, { timeout: 60_000, interval: 1_000 }); 614}`},{id:"app",label:"app/rusd/page.tsx",language:"tsx",code:`'use client'; 615 616export default function RusdPage() { 617 const [walletBalance, setWalletBalance] = useState(null); 618 const [channelBalance, setChannelBalance] = useState(null); 619 const [payment, setPayment] = useState('Not sent'); 620 621 async function refresh() { 622 setWalletBalance(await queryRusdBalance(nodeInfo.default_funding_lock_script)); 623 setChannelBalance((await findRusdChannel(node)).local_balance); 624 } 625 626 async function pay() { 627 const result = await sendRusd(node, '1'); 628 setPayment(result.status); 629 await refresh(); 630 } 631 632 return <RusdBalances wallet={walletBalance} channel={channelBalance} payment={payment} />; 633}`}],h={asset:{file:"asset",start:1,end:12},fund:{file:"asset",start:7,end:12},channel:{file:"channel",start:1,end:10},payment:{file:"payment",start:1,end:11},react:{file:"app",start:4,end:20}};e.s(["FiberRusdPaymentTutorial",0,function(){let e=(0,s.useFiberRoutingNode)("fiber-docs:rusd-payment-v1"),[p,f]=(0,n.useState)(null),[m,x]=(0,n.useState)("1"),[b,j]=(0,n.useState)("Not sent"),[y,g]=(0,n.useState)("idle"),[v,w]=(0,n.useState)([]),N=(0,n.useMemo)(()=>(0,i.findMatchingChannel)(e.channels,s.bottlePeer.pubkey,e=>!!e.funding_udt_type_script),[e.channels]),C=(0,i.isChannelReady)(N),S=e.peers.some(e=>(0,s.samePubkey)(e,s.bottlePeer.pubkey)),k=(0,n.useCallback)(e=>w(t=>[...t.slice(-10),e]),[]),T=(0,n.useCallback)(async()=>{if(e.nodeInfo)try{f(await d(e.nodeInfo.default_funding_lock_script))}catch(t){e.setError(t instanceof Error?t.message:"Unable to query RUSD.")}},[e]);(0,n.useEffect)(()=>{e.nodeInfo&&T()},[T,e.nodeInfo]),(0,n.useEffect)(()=>{N&&g((0,i.progressFromChannelState)(N))},[N]);let R=(0,n.useCallback)(async()=>{let t=e.nodeRef.current;if(!t)return;if(g("connecting"),!(S||await e.connect(s.bottlePeer)))return g("error");let n=(0,i.findMatchingChannel)((await t.listChannels()).channels,s.bottlePeer.pubkey,e=>!!e.funding_udt_type_script);if(n)return g((0,i.progressFromChannelState)(n)),k(`Reused ${n.state.state_name}`),void e.refresh();g("submitting");let a=await e.run("open RUSD channel",e=>e.openChannel({pubkey:s.bottlePeer.pubkey,funding_amount:o("20"),funding_udt_type_script:l,public:!0}));if(!a)return g("error");g("confirming"),k(`20 RUSD channel submitted \xb7 ${(0,s.shorten)(a.temporary_channel_id)}`),await e.refresh(),await T()},[k,S,T,e]),P=(0,n.useCallback)(async()=>{j("Sending");let t=await e.run("send RUSD",e=>e.sendPayment({target_pubkey:s.bottlePeer.pubkey,amount:o(m),udt_type_script:l,keysend:!0}));if(!t)return j("Failed");let n="Success"===t.status||"Failed"===t.status?t:await e.nodeRef.current.waitForPayment(t.payment_hash,{timeout:6e4,interval:1e3});j(n.status),k(`${n.status} \xb7 ${m} RUSD \xb7 ${(0,s.shorten)(n.payment_hash)}`),await e.refresh(),await T()},[k,m,T,e]),B=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("header",{className:r.default.hero,"data-tutorial-section":"intro",children:[(0,t.jsxs)("div",{className:r.default.eyebrow,children:[(0,t.jsx)("span",{children:"Stablecoin"}),(0,t.jsx)("span",{className:r.default.eyebrowRule}),(0,t.jsx)("span",{children:"25 minute tutorial"})]}),(0,t.jsx)("h1",{children:"Pay with RUSD over Fiber"}),(0,t.jsx)("p",{className:r.default.lead,children:"Fund a Fiber channel with Testnet RUSD, inspect token balances, and send a stablecoin payment to Bottle."}),(0,t.jsxs)("div",{className:r.default.heroMeta,children:[(0,t.jsx)("span",{children:"RUSD"}),(0,t.jsx)("span",{children:"UDT channel"}),(0,t.jsx)("span",{children:"Keysend"})]})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"asset",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"1"})," Identify the asset"]}),(0,t.jsx)("h2",{children:"A Fiber UDT is defined by its type script"}),(0,t.jsx)("p",{children:"CKB and RUSD use the same channel APIs, but an RUSD request includes the Testnet RUSD type script. The scriptânot a t
633icker stringâidentifies the asset."}),(0,t.jsx)("small",{className:r.default.fileReference,children:"lib/rusd.ts · lines 1â12"})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"fund",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"2"})," Fund the browser identity"]}),(0,t.jsx)("h2",{children:"Provide both CKB capacity and Testnet RUSD"}),(0,t.jsx)("p",{children:"CKB pays for cells and transaction fees. RUSD supplies channel liquidity. The demo queries matching UDT cells and decodes each little-endian amount."}),(0,t.jsxs)("div",{className:r.default.note,children:[(0,t.jsx)("strong",{children:"Testnet assets only"}),(0,t.jsx)("p",{children:"Use the Nervos faucet for CKB and the Stable++ Testnet faucet for RUSD."})]}),(0,t.jsx)("small",{className:r.default.fileReference,children:"lib/rusd.ts · lines 7â12"})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"channel",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"3"})," Open the RUSD channel"]}),(0,t.jsx)("h2",{children:"Pass the asset type script when funding"}),(0,t.jsxs)("p",{children:["The 20 RUSD amount is expressed in its smallest unit and paired with ",(0,t.jsx)("code",{children:"funding_udt_type_script"}),". The browser reuses an existing compatible channel after refresh."]}),(0,t.jsx)("small",{className:r.default.fileReference,children:"lib/channel.ts · lines 1â10"})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"payment",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"4"})," Send RUSD"]}),(0,t.jsx)("h2",{children:"Keep the same asset on the payment"}),(0,t.jsxs)("p",{children:["The payment includes ",(0,t.jsx)("code",{children:"udt_type_script"})," so routing selects RUSD liquidity rather than a CKB channel."]}),(0,t.jsx)("small",{className:r.default.fileReference,children:"lib/payment.ts · lines 1â11"})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"react",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"5"})," Reconcile balances"]}),(0,t.jsx)("h2",{children:"Show wallet and channel liquidity separately"}),(0,t.jsx)("p",{children:"On-chain RUSD decreases when it enters the channel; the channel local balance decreases as payments are sent. The interface keeps both layers visible."}),(0,t.jsx)("small",{className:r.default.fileReference,children:"app/rusd/page.tsx · lines 4â20"})]})]}),_=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:r.default.panelHeader,children:[(0,t.jsxs)("span",{children:[(0,t.jsx)("i",{className:r.default.liveDot})," RUSD Testnet flow"]}),(0,t.jsx)("button",{className:r.default.headerAction,disabled:!e.nodeInfo,onClick:()=>{e.refresh(),T()},children:"Refresh"})]}),(0,t.jsxs)("div",{className:`${r.default.previewStage} ${r.default.paymentPreviewStage}`,children:[(0,t.jsxs)("div",{className:r.default.paymentCard,children:[(0,t.jsxs)("div",{className:r.default.routeStatusGrid,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Node"}),(0,t.jsx)("strong",{children:e.nodeState})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Wallet RUSD"}),(0,t.jsx)("strong",{children:c(p)})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Channel"}),(0,t.jsx)("strong",{children:N?.state.state_name??"Not opened"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Payment"}),(0,t.jsx)("strong",{children:b})]})]}),(0,t.jsxs)("div",{className:r.default.paymentFlow,children:[(0,t.jsx)("div",{className:r.default.paymentFlowNumber,children:"1"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("strong",{children:"Start and fund the browser identity"}),(0,t.jsx)("span",{children:e.address?`${(0,s.hexToCkb)(e.balance)} CKB \xb7 ${c(p)} RUSD`:"Start to derive the funding address."})]}),(0,t.jsxs)("div",{className:r.default.compactActions,children:[(0,t.jsx)("button",{className:r.default.startButton,disabled:!!e.nodeInfo||!!e.busy,onClick:e.start,children:"Start"}),(0,t.jsx)("a",{className:r.default.faucetButton,href:"https://faucet.nervos.org",rel:"noreferrer",target:"_blank",children:"CKB â
633"}),(0,t.jsx)("a",{className:r.default.faucetButton,href:"https://testnet0815.stablepp.xyz/faucet",rel:"noreferrer",target:"_blank",children:"RUSD â"})]})]}),e.address&&(0,t.jsxs)("div",{className:r.default.rebalanceAddress,children:[(0,t.jsx)("code",{children:e.address}),(0,t.jsx)("button",{onClick:()=>void navigator.clipboard.writeText(e.address),children:"Copy"})]}),(0,t.jsxs)("div",{className:r.default.paymentFlow,children:[(0,t.jsx)("div",{className:r.default.paymentFlowNumber,children:"2"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("strong",{children:"Open a 20 RUSD channel"}),(0,t.jsx)("span",{children:"Public channel · Bottle acceptor"})]}),(0,t.jsx)("button",{className:r.default.paymentButton,disabled:!e.nodeInfo||(p??0n)<20n*100000000n||!!N||!!e.busy,onClick:()=>void R(),children:"connecting"===y?"Connectingâ¦":"submitting"===y?"Openingâ¦":"confirming"===y?"Confirmingâ¦":C?"Ready":"Connect & open"})]}),(0,t.jsx)(i.ChannelProgress,{label:"RUSD channel progress",stage:y}),(0,t.jsxs)("div",{className:r.default.oneWayChannelFacts,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Wallet balance"}),(0,t.jsxs)("strong",{children:[c(p)," RUSD"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Channel local"}),(0,t.jsxs)("strong",{children:[c(N?BigInt(N.local_balance):null)," RUSD"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Channel remote"}),(0,t.jsxs)("strong",{children:[c(N?BigInt(N.remote_balance):null)," RUSD"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Asset"}),(0,t.jsx)("strong",{children:"Testnet RUSD"})]})]}),(0,t.jsxs)("div",{className:r.default.paymentFlow,children:[(0,t.jsx)("div",{className:r.default.paymentFlowNumber,children:"3"}),(0,t.jsxs)("div",{className:r.default.paymentFlowBody,children:[(0,t.jsx)("strong",{children:"Send stablecoin liquidity"}),(0,t.jsxs)("label",{children:[(0,t.jsx)("input",{disabled:!C,inputMode:"decimal",onChange:e=>x(e.target.value),value:m}),(0,t.jsx)("span",{children:"RUSD"})]})]}),(0,t.jsx)("button",{className:r.default.paymentButton,disabled:!C||!!e.busy,onClick:()=>void P(),children:"send RUSD"===e.busy?"Sendingâ¦":"Send RUSD"})]}),e.error&&(0,t.jsx)("div",{className:r.default.paymentError,children:e.error})]}),(0,t.jsxs)("div",{className:r.default.eventPanel,children:[(0,t.jsxs)("div",{className:r.default.eventPanelHeader,children:[(0,t.jsx)("span",{children:"Asset events and results"}),(0,t.jsx)("i",{className:r.default.liveDot})]}),(0,t.jsx)("div",{className:r.default.eventList,children:v.map((e,n)=>(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:String(n+1).padStart(2,"0")}),(0,t.jsx)("code",{children:"rusd"}),(0,t.jsx)("span",{children:e})]},`${e}-${n}`))})]})]})]});return(0,t.jsx)(a.RoutingTutorialFrame,{article:B,codeFiles:u,currentTutorialIndex:7,defaultFile:"asset",demoDescription:"Fund a real RUSD channel on Testnet and send stablecoin liquidity through Fiber.",demoTitle:"Run the RUSD Payment Demo",downloadHref:"/downloads/fiber-rusd-payment.zip",liveDemo:_,nextHref:"/docs/build/close-channel",previousHref:"/docs/build/verified-result-payment",sectionCode:h})}])},37944,e=>{"use strict";var t=e.i(99873),n=e.i(15300),a=e.i(29764),s=e.i(49083),i=e.i(80296),r=e.i(84678);let l=[{id:"channels",label:"lib/channels.ts",language:"typescript",code:`export async function listEveryChannel(node) { 634 return node.listChannels({ include_closed: true }); 635} 636 637export function findRestorableChannel(channels, pubkey) { 638 return channels.find(channel => 639 samePubkey(channel.pubkey, pubkey) && 640 channel.is_one_way && 641 !['CLOSED', 'SHUTTING_DOWN'].includes(channel.state.state_name) 642 ); 643}`},{id:"close",label:"lib/close.ts",language:"typescript",code:`export async function closeCooperatively(node, channel) { 644 await node.shutdownChannel({ 645 channel_id: channel.channel_id, 646 force: false, 647 }); 648} 649 650export async function waitForClosed(node, channelId) { 651 while (true) { 652 const { channels } = await node.listChannels({ include_closed: true }); 653 const channel = channels.find(item => item.channel_id === channelId); 654 if (channel?.state.state_name === 'CLOSED') return channel; 655 await new Promise(resolve => setTimeout(resolve, 3_000)); 656 } 657}`},{id:"balance",label:"lib/recovery.ts",language:"typescript",code:`export async function observeRecovery(node, channel) { 658 const before = await queryCkbBalance(node.fundingLockScript); 659 await closeCooperatively(node, channel); 660 const closed = await waitForClosed(node, channel.channel_id); 661 const after = await queryCkbBalance(node.fundingLockScript); 662 return { 663 before, 664 after, 665 recovered: after - before, 666 transaction: closed.shutdown_transaction_hash, 667 }; 668}`},{id:"app",label:"app/close/page.tsx",language:"tsx",code:`'use client'; 669 670export default function CloseChannelPage() { 671 const [state, setState] = useState('CHANNEL_READY'); 672 const [before, setBefore] = useState(null); 673 const [after, setAfter] = useState(null); 674 675 async function close() { 676 setBefore(await queryBalance()); 677 await closeCooperatively(node, selectedChannel); 678 setState('SHUTTING_DOWN'); 679 } 680 681 useEffect(() =>
681 pollClosedChannel({ 682 includeClosed: true, 683 onChannel(channel) { setState(channel.state.state_name); }, 684 onBalance: setAfter, 685 }), []); 686 687 return <RecoveryResult state={state} before={before} after={after} />; 688}`}],o={restore:{file:"channels",start:1,end:11},close:{file:"close",start:1,end:6},confirm:{file:"close",start:8,end:16},recover:{file:"balance",start:1,end:12},react:{file:"app",start:4,end:20}};e.s(["FiberCloseChannelTutorial",0,function(){let e=(0,s.useFiberRoutingNode)("fiber-docs:unidirectional-channel-v1"),[c,d]=(0,n.useState)([]),[u,h]=(0,n.useState)("idle"),[p,f]=(0,n.useState)(null),[m,x]=(0,n.useState)(null),[b,j]=(0,n.useState)([]),y=(0,n.useCallback)(e=>j(t=>[...t.slice(-10),e]),[]),g=(0,n.useMemo)(()=>(0,i.findMatchingChannel)(e.channels,s.bottlePeer.pubkey,e=>e.is_one_way),[e.channels]),v=(0,n.useMemo)(()=>g||c.filter(e=>(0,s.samePubkey)(e.pubkey,s.bottlePeer.pubkey)&&e.is_one_way).sort((e,t)=>Number(BigInt(t.created_at)-BigInt(e.created_at)))[0],[c,g]),w=(0,i.isChannelReady)(v),N=v?.state.state_name??"Not opened",C=(0,i.normalizedChannelState)(N),S="shuttingdown"===C,k="closed"===C,T=e.peers.some(e=>(0,s.samePubkey)(e,s.bottlePeer.pubkey)),R=(0,n.useCallback)(async()=>{let t=e.nodeRef.current;t&&(d((await t.listChannels({include_closed:!0})).channels),await e.refresh())},[e]);(0,n.useEffect)(()=>{v&&h((0,i.progressFromChannelState)(v))},[v]),(0,n.useEffect)(()=>{if(!e.nodeInfo)return;R();let t=window.setInterval(()=>void R(),3e3);return()=>window.clearInterval(t)},[R,e.nodeInfo]);let P=(0,n.useCallback)(async()=>{let t=e.nodeRef.current;if(!t)return;if(h("connecting"),!(T||await e.connect(s.bottlePeer)))return h("error");let n=(0,i.findMatchingChannel)((await t.listChannels()).channels,s.bottlePeer.pubkey,e=>e.is_one_way);if(n)return h((0,i.progressFromChannelState)(n)),y(`Reused ${n.state.state_name}`),void R();h("submitting");let a=await e.run("open recovery channel",e=>e.openChannel({pubkey:s.bottlePeer.pubkey,funding_amount:(0,s.ckbToHex)("499"),public:!1,one_way:!0}));if(!a)return h("error");h("confirming"),y(`Channel submitted \xb7 ${(0,s.shorten)(a.temporary_channel_id)}`),await R()},[y,T,R,e]),B=(0,n.useCallback)(async()=>{v&&e.nodeRef.current&&(f(e.balance),x(null),h("closing"),y("Cooperative shutdown requested"),await e.run("close channel",e=>e.shutdownChannel({channel_id:v.channel_id,force:!1})),await R())},[y,R,e,v]),_=(0,n.useCallback)(async()=>{y("Reconnecting to Bottle to continue cooperative shutdown"),y(await e.connect(s.bottlePeer)?"Bottle connected; waiting for shutdown confirmation":"Bottle is still offline; retry in a moment"),await R()},[y,R,e]);(0,n.useEffect)(()=>{k&&null!==p&&(x(e.balance),y(`Channel closed \xb7 ${(0,s.shorten)(v?.shutdown_transaction_hash??"")}`))},[y,p,k,e.balance,v?.shutdown_transaction_hash]);let F=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("header",{className:r.default.hero,"data-tutorial-section":"intro",children:[(0,t.jsxs)("div",{className:r.default.eyebrow,children:[(0,t.jsx)("span",{children:"Channel lifecycle"}),(0,t.jsx)("span",{className:r.default.eyebrowRule}),(0,t.jsx)("span",{children:"20 minute tutorial"})]}),(0,t.jsx)("h1",{children:"Close a Fiber Channel and Recover Funds"}),(0,t.jsx)("p",{className:r.default.lead,children:"Restore an existing one-way channel, request a cooperative shutdown, and follow its balance back to the browser funding address."}),(0,t.jsxs)("div",{className:r.default.heroMeta,children:[(0,t.jsx)("span",{children:"Shutdown"}),(0,t.jsx)("span",{children:"Recovery"}),(0,t.jsx)("span",{children:"On-chain"})]})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"restore",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"1"})," Restore before creating"]}),(0,t.jsx)("h2",{children:"Look for an existing channel after refresh"}),(0,t.jsx)("p",{children:"The browser starts with the same persisted identity as the one-way tutorials. It first lists active channels and reuses any compatible opening or ready channel."}),(0,t.jsx)("small",{className:r.default.fileReference,children:"lib/channels.ts · lines 1â11"})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"close",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"2"})," Request shutdown"]}),(0,t.jsx)("h2",{children:"Prefer a cooperative close"}),(0,t.jsxs)("p",{children:[(0,t.jsx)("code",{children:"shutdownChannel
688()"})," asks both peers to agree on final balances and publish the closing transaction. This tutorial intentionally leaves force close out of the primary flow."]}),(0,t.jsx)("small",{className:r.default.fileReference,children:"lib/close.ts · lines 1â6"})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"confirm",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"3"})," Follow every state"]}),(0,t.jsx)("h2",{children:"Keep closed channels in the query"}),(0,t.jsxs)("p",{children:["Normal active-channel lists eventually drop the channel. Poll with ",(0,t.jsx)("code",{children:"include_closed: true"})," so the UI can show ",(0,t.jsx)("code",{children:"SHUTTING_DOWN â CLOSED"})," and the shutdown transaction hash."]}),(0,t.jsx)("small",{className:r.default.fileReference,children:"lib/close.ts · lines 8â16"})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"recover",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"4"})," Reconcile funds"]}),(0,t.jsx)("h2",{children:"Observe the on-chain balance before and after"}),(0,t.jsx)("p",{children:"Closing returns the final local allocation to the configured shutdown script, minus on-chain fees. Indexing may lag behind the channel state, so the page continues polling."}),(0,t.jsx)("small",{className:r.default.fileReference,children:"lib/recovery.ts · lines 1â12"})]}),(0,t.jsxs)("section",{className:r.default.section,"data-tutorial-section":"react",children:[(0,t.jsxs)("div",{className:r.default.stepLabel,children:[(0,t.jsx)("span",{children:"5"})," Make completion explicit"]}),(0,t.jsx)("h2",{children:"Show state, transaction, and recovered balance"}),(0,t.jsx)("p",{children:"The final panel preserves the closed channel record and compares funding-address snapshots instead of making the channel disappear."}),(0,t.jsx)("small",{className:r.default.fileReference,children:"app/close/page.tsx · lines 4â20"})]})]}),I=null!==p&&null!==m?m-p:null,$=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:r.default.panelHeader,children:[(0,t.jsxs)("span",{children:[(0,t.jsx)("i",{className:r.default.liveDot})," Cooperative channel shutdown"]}),(0,t.jsx)("button",{className:r.default.headerAction,disabled:!e.nodeInfo,onClick:()=>void R(),children:"Refresh"})]}),(0,t.jsxs)("div",{className:`${r.default.previewStage} ${r.default.paymentPreviewStage}`,children:[(0,t.jsxs)("div",{className:r.default.paymentCard,children:[(0,t.jsxs)("div",{className:r.default.routeStatusGrid,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Node"}),(0,t.jsx)("strong",{children:e.nodeState})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Peer"}),(0,t.jsx)("strong",{children:T?"Connected":"Offline"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Channel"}),(0,t.jsx)("strong",{children:N})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Recovery"}),(0,t.jsx)("strong",{children:k?"Closed":"closing"===u?"Pending":"Not started"})]})]}),(0,t.jsxs)("div",{className:r.default.paymentFlow,children:[(0,t.jsx)("div",{className:r.default.paymentFlowNumber,children:"1"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("strong",{children:"Restore or prepare a one-way channel"}),(0,t.jsx)("span",{children:e.address?`${(0,s.hexToCkb)(e.balance)} CKB \xb7 ${(0,s.shorten)(e.address,14,10)}`:"Start the persisted browser identity."})]}),(0,t.jsxs)("div",{className:r.default.compactActions,children:[(0,t.jsx)("button",{className:r.default.startButton,disabled:!!e.nodeInfo||!!e.busy,onClick:e.start,children:"Start"}),(0,t.jsx)("a",{className:r.default.faucetButton,href:"https://faucet.nervos.org",rel:"noreferrer",target:"_blank",children:"Faucet â"}),(0,t.jsx)("button",{className:r.default.paymentButton,disabled:!e.nodeInfo||!!g||S||!!e.busy||(e.balance??0n)<BigInt((0,s.ckbToHex)("499")),onClick:()=>void P(),children:w?"Channel ready":S?"Shutdown pending":"Prepare channel"})]})]}),e.address&&(0,t.jsxs)("div",{className:r.default.rebalanceAddress,children:[(0,t.jsx)("code",{children:e.address}),(0,t.jsx)("button",{onClick:()=>void navigator.clipboard.writeText(e.address),children:"Copy"})]}),!k&&(0,t.jsx)(i.ChannelProgress,{label:"Channel preparation",stage:u}),(0,t.jsxs)("div",{className:r.default.oneWayChannelFacts,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Channel ID"}),(0,t.jsx)("strong",{title:v?.channel_id,children:(0,s.shorten)(v?.channel_id??"")})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Local balance"}),(0,t.jsxs)("strong",{children:[(0,s.hexToCkb)(v?.local_balance)," CKB"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"Shutdown tx"}),(0,t.jsx)("strong",{title:v?.shutdown_transaction_hash??"",children:(0,s.shorten)(v?.shutdown_transaction_hash??"")})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{children:"State"}),(0,t.jsx)("strong",{children:N})]})]}),(0,t.jsxs)("div",{className:r.default.paymentFlow,children:[(0,t.jsx)("div",{className:r.default.paymentFlowNumber,children:"2"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("strong",{children:"Close cooperatively"}),(0,t.jsx)("span",{children:S?T?"Shutdown was submitted. Waiting for the closing transaction to confirm on-chain.":"Shutdown is pending, but Bottle is offline. Reconnect to continue the cooperative close.":k?"The channel is closed and its final balance is returning on-chain.":"Both peers agree on final balances before returning funds on-chain."})]}),(0,t.jsx)("button",{className:r.default.paymentButton,disabled:k||!!e.busy||!w&&!(S&&!T),onClick:()=>void(S?_():B()),children:e.busy.startsWith("connect:")?"Reconnectingâ¦":"close channel"===e.busy?"Submittingâ¦":k?"Closed â":S&&!T?"Reconnect & continue":S?"Waiting on-chainâ¦":"Close channel"})]}),null!==p&&(0,t.jsxs)("div",{className:r.default.balanceComparison,children:[(0,t.jsx)("span",{children:"Funding address"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("b",{children:"Before â after"}
688),(0,t.jsxs)("code",{children:[(0,s.hexToCkb)(p)," â ",(0,s.hexToCkb)(m)," CKB"]}),(0,t.jsx)("i",{children:null===I?"Waiting for indexer":`${I>=0n?"+":""}${(0,s.hexToCkb)(I)} CKB`})]})]}),e.error&&(0,t.jsx)("div",{className:r.default.paymentError,children:e.error})]}),(0,t.jsxs)("div",{className:r.default.eventPanel,children:[(0,t.jsxs)("div",{className:r.default.eventPanelHeader,children:[(0,t.jsx)("span",{children:"Shutdown events and results"}),(0,t.jsx)("i",{className:r.default.liveDot})]}),(0,t.jsxs)("div",{className:r.default.eventList,children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:"CH"}),(0,t.jsx)("code",{children:"state"}),(0,t.jsx)("span",{children:N})]}),b.map((e,n)=>(0,t.jsxs)("div",{children:[(0,t.jsx)("time",{children:String(n+1).padStart(2,"0")}),(0,t.jsx)("code",{children:"close"}),(0,t.jsx)("span",{children:e})]},`${e}-${n}`))]})]})]})]});return(0,t.jsx)(a.RoutingTutorialFrame,{article:F,codeFiles:l,currentTutorialIndex:8,defaultFile:"close",demoDescription:"Cooperatively close a real Testnet channel and watch its final balance return on-chain.",demoTitle:"Run the Channel Recovery Demo",downloadHref:"/downloads/fiber-close-channel.zip",liveDemo:$,previousHref:"/docs/build/rusd-payment",sectionCode:o})}])}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.