PageSourceSearch

https://www.fiber.world/_next/static/chunks/03w7_a44q9c8g.js?dpl=dpl_BCgDLpW99AGndbPcbaijeS88NYfS

js fiber.world collected 2026-10-04 00:15:19 UTC 159,588 bytes, 688 lines download raw bytes

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.