Capture the current tab — visible viewport, entire scrollable page, or a rectangle
you draw. The capture opens in a full-tab editor: crop, annotate, line tool, resizable text,
redact, save.
Signed update manifest — SHA-256 verified before install
@@ -133,19 +119,58 @@
📝 Notepad bundled 0.1.0
Right-sidebar notes. Text lives on this machine only and autosaves as you type.
-
Extension · Settings › Extensions · no update channel yet — ships with each Theseus release.
+
Settings › Extensions · no update channel yet — ships with each Theseus release.
+
-
Installing something else. An extension is a folder with an addon.json.
- Drop it into the extensions directory (Settings › Extensions › Open extensions folder ), press
- Reload , and it is live. Extensions run with full app access — treat one like an unsigned executable
- and load only what you trust.
+
+
+
+ Build and publish your own
+ An extension is a folder with an addon.json and a main script. Package it, sign it with
+ the wallet that holds one of your BCDN names, and it is listed above within a minute. Nothing to apply for.
+
+
+ Write it. The manifest and a CommonJS entry that exports activate(api):
+my-ext/
+ addon.json {"id":"my-ext","name":"My Extension","version":"1.0.0",
+ "description":"…","author":"you.bch","icon":"✨",
+ "main":"index.js","capabilities":["sidebar-panel"]}
+ index.js module.exports = { activate(api) {
+ api.registerSidebarPanel({ id:"main", title:"My Extension", icon:"✨", page:"panel.html" });
+ } };
+ panel.html plain HTML, runs in the sidebar; window.silentmode.storage for local state
+ The bundled Notepad
+ is the smallest complete example; addons-host.js
+ is the full API. id is [a-z0-9._-], 2–64 chars, and belongs to the first name that publishes it.
+ Test it locally. Settings › Extensions › Open extensions folder , drop the folder in, Reload .
+ Package it. tar -czf my-ext-1.0.0.tar.gz -C my-ext . (max 8 MB; addon.json at the root or in a single top folder).
+ Publish it. Open Publish an extension →
+ sign in with the recovery phrase of the wallet that holds your name, pick the package, and sign. The gateway
+ verifies the signature against the name's on-chain owner, stores the package on Sia, and updates the catalog.
+ New versions: same page, higher version number, same name.
+
+
+ Prefer the command line? The upload is one signed PUT:
+ PUT https://silentmode.st/api/ext/<your-name>/<id>/<version> with the tarball as body and headers
+ x-bns-ts, x-bns-sig (over sha256("BNS-EXT1\n<name>\n<id>\n<version>\n<sha256>\n<ts>"))
+ and x-bns-entry-sig (over sha256("silentmode.extension-v1|<id>|<version>|<sha256>|<name>")),
+ both 65-byte BCH message signatures by the name's owner key.
diff --git a/extensions/js/bns-register.js b/extensions/js/bns-register.js
new file mode 100644
index 00000000..46da9b18
--- /dev/null
+++ b/extensions/js/bns-register.js
@@ -0,0 +1,7 @@
+import{generateBip39Mnemonic as qe,deriveSeedFromBip39Mnemonic as ne,deriveHdPrivateNodeFromSeed as Ke,deriveHdPath as We,secp256k1 as oe,hash160 as ze,encodeCashAddress as no,lockingBytecodeToCashAddress as Ge,cashAddressToLockingBytecode as oo,binToHex as je,hexToBin as so,utf8ToBin as ie,binToBase64 as Nt,base64ToBin as Dt}from"@bitauth/libauth";var mt="bchtest";var $t="m/44'/145'/0'",Je=5,kt="bns.wallet.v1";function ae(e){let t=ze(e);return Uint8Array.from([118,169,20,...t,136,172])}function se(e,t,n){let o=Ge({bytecode:ae(e),prefix:t,tokenSupport:n});if(typeof o=="string")throw new Error(`address encoding failed: ${o}`);return o.address}var Rt=class e{constructor(t,n,o=mt,s=$t){this.mnemonic=t,this.prefix=o,this.accountPath=s,this._root=Ke(n),this._keys=new Map,this._derive()}static async create(t=mt,n=$t){let o=qe();return new e(o,await ne(o),t,n)}static async fromMnemonic(t,n=mt,o=$t){let s=String(t).trim().toLowerCase().replace(/\s+/g," "),r=await ne(s);if(typeof r=="string")throw new Error(`invalid recovery phrase: ${r}`);return new e(s,r,n,o)}_derive(){this.receive=[],this.change=[];for(let[t,n]of[[0,this.receive],[1,this.change]])for(let o=0;o
t.address)}keyFor(t){let n=this._keys.get(t);return n?{privateKey:n.privateKey,publicKey:n.publicKey}:null}signMessage(t,n){if(!(t instanceof Uint8Array)||t.length!==32)throw new Error("signMessage expects a 32-byte Uint8Array digest");let o=n||this.address,s=[...this.receive,...this.change].find(h=>h.address===o);if(!s)throw new Error(`no key for address ${o}`);let r=oe.signMessageHashRecoverableCompact(s.privateKey,t);if(typeof r=="string")throw new Error(`signMessage failed: ${r}`);let{signature:a}=r,i=r.recoveryId??r.recovery;if(typeof i!="number")throw new Error("signMessage: no recovery id in signature result");let l=31+i,c=new Uint8Array(65);return c[0]=l,c.set(a,1),c}async encrypt(t){let n=crypto.getRandomValues(new Uint8Array(16)),o=crypto.getRandomValues(new Uint8Array(12)),s=await re(t,n),r=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:o},s,ie(this.mnemonic)));return JSON.stringify({v:1,kdf:"PBKDF2-SHA256",iterations:Ft,prefix:this.prefix,salt:Nt(n),iv:Nt(o),ct:Nt(r)})}static async decrypt(t,n){let o=typeof t=="string"?JSON.parse(t):t,s=await re(n,Dt(o.salt),o.iterations??Ft),r;try{r=await crypto.subtle.decrypt({name:"AES-GCM",iv:Dt(o.iv)},s,Dt(o.ct))}catch{throw new Error("wrong password")}return e.fromMnemonic(new TextDecoder().decode(r),o.prefix??mt)}async save(t,n=globalThis.localStorage){n.setItem(kt,await this.encrypt(t))}static async load(t,n=globalThis.localStorage){let o=n.getItem(kt);return o?e.decrypt(o,t):null}static exists(t=globalThis.localStorage){return!!t?.getItem(kt)}static forget(t=globalThis.localStorage){t.removeItem(kt)}},Ft=25e4;async function re(e,t,n=Ft){let o=await crypto.subtle.importKey("raw",ie(String(e)),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:t,iterations:n,hash:"SHA-256"},o,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}var bt=["wss://silentmode.st/electrum","wss://chipnet.bch.ninja:50004","wss://chipnet.imaginary.cash:50004","wss://tbch4.loping.net:62002"],Lt=class{constructor(t,n){this.url=t,this.WS=n||globalThis.WebSocket,this.id=0,this.pending=new Map,this.buf=""}connect(){return new Promise((t,n)=>{let o=new this.WS(this.url);this.ws=o;let s=async()=>{try{await this.call("server.version",["bns-web","1.4.2"]),t(this)}catch(i){n(i)}},r=i=>n(new Error("electrum ws error: "+(i?.message||this.url))),a=i=>this._onData(typeof i.data=="string"?i.data:i.data.toString());o.addEventListener?(o.addEventListener("open",s),o.addEventListener("error",r),o.addEventListener("message",a)):(o.onopen=s,o.onerror=r,o.onmessage=a)})}_handle(t){let n=this.pending.get(t.id);n&&(this.pending.delete(t.id),t.error?n.reject(new Error(JSON.stringify(t.error))):n.resolve(t.result))}_onData(t){this.buf+=t;let n;for(;(n=this.buf.indexOf(`
+`))>=0;){let s=this.buf.slice(0,n).trim();if(this.buf=this.buf.slice(n+1),s)try{this._handle(JSON.parse(s))}catch{}}let o=this.buf.trim();if(o)try{let s=JSON.parse(o);this._handle(s),this.buf=""}catch{}}call(t,n=[]){let o=++this.id;return new Promise((s,r)=>{this.pending.set(o,{resolve:s,reject:r}),this.ws.send(JSON.stringify({id:o,method:t,params:n})+`
+`),setTimeout(()=>{this.pending.has(o)&&(this.pending.delete(o),r(new Error(`electrum timeout: ${t}`)))},2e4)})}close(){try{this.ws.close()}catch{}}};async function ce(e=bt,t){let n=[];for(let o of e)try{return await new Lt(o,t).connect()}catch(s){n.push(`${o}: ${s?.message??s}`)}throw Pe(e.length,n)}function Pe(e,t){let n=new Error(`no electrum server reachable (tried ${e}). These use non-standard ports, which some networks and VPNs block:
+ ${t.join(`
+ `)}`);return n.code="NO_ELECTRUM",n.failures=t,n}import{cashAddressToLockingBytecode as Ye,lockingBytecodeToCashAddress as Xe,sha256 as Ve,hash160 as co,binToHex as Qe,hexToBin as lo}from"@bitauth/libauth";function le(e){return Qe(Ve.hash(e).slice().reverse())}function _(e){let t=Ye(e);if(typeof t=="string")throw new Error(`bad address "${e}": ${t}`);return t.bytecode}function Ze(e){return le(_(e))}function M(e){let t=_(e),n=String(e).includes(":")?e.split(":")[0]:"bchtest",o=Xe({bytecode:t,prefix:n,tokenSupport:!0});if(typeof o=="string")throw new Error(`cannot token-encode "${e}": ${o}`);return o.address}async function ue(e,t){let n=_(t),o=le(n);return(await e.call("blockchain.scripthash.listunspent",[o,"include_tokens"])).map(r=>({txid:r.tx_hash,vout:r.tx_pos,satoshis:BigInt(r.value),height:r.height,address:t,lockingBytecode:n,token:r.token_data?{category:r.token_data.category,amount:BigInt(r.token_data.amount??0),nft:r.token_data.nft?{capability:r.token_data.nft.capability,commitment:r.token_data.nft.commitment}:void 0}:void 0}))}async function z(e,t){let n=[];for(let o of t)n.push(...await ue(e,o));return n}async function V(e,t){return e.call("blockchain.transaction.broadcast",[t])}import{binToHex as vt,hexToBin as j,utf8ToBin as st,binToUtf8 as de,encodeTransaction as tn,decodeTransaction as po}from"@bitauth/libauth";var en="BNS1",Tt="bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl",Mt="bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh",I=600n,F=1000n,U=546n,Ct="bch",Ht=200,nn=/^[a-z0-9-]{1,32}$/,he=/^[a-z0-9-]{1,16}$/;function ht(e){let t=String(e).toLowerCase().trim().replace(/^\.+/,"").replace(/\.+$/,"");if(!he.test(t))throw new Error(`invalid TLD "${e}" (allowed: [a-z0-9-]{1,16})`);return t}var St=class extends Error{constructor(t){super(t),this.name="NoGenesisInputError",this.code="NO_GENESIS_INPUT"}},J=class extends Error{constructor(t,{needSats:n,haveSats:o}={}){super(t),this.name="InsufficientFundsError",this.code="INSUFFICIENT_FUNDS",this.needSats=n,this.haveSats=o}};function P(e){let t=String(e).toLowerCase().trim().replace(/\.$/,"").split(".").filter(Boolean),n,o;if(t.length===0)throw new Error(`invalid name "${e}"`);if(t.length===1?(n=t[0],o=Ct):(o=t[t.length-1],n=t[t.length-2]),!nn.test(n)||!he.test(o))throw new Error(`invalid name "${e}" (allowed: ., [a-z0-9-])`);return o===Ct?n:`${n}.${o}`}function Y(e){return e.includes(".")?e:`${e}.${Ct}`}function it(e,t,n){let o=JSON.stringify({o:e,n:t,r:n??{}}),s=st(en+o);if(s.length>Ht)throw new Error(`records too large (${s.length} > ${Ht} bytes)`);return s}function Ut(e,t="REG"){return Ht-it(t,e,{}).length}function on(e){return e.length<=75?Uint8Array.from([e.length,...e]):e.length<=255?Uint8Array.from([76,e.length,...e]):Uint8Array.from([77,e.length&255,e.length>>8&255,...e])}function pt(e){return Uint8Array.from([106,...on(e)])}function qt(){return Uint8Array.from([65,...new Uint8Array(65),33,...new Uint8Array(33)])}var sn=e=>!e.token;function q(e,t,{requireGenesis:n=!0}={}){let o=e.filter(sn),s=[],r=0n;if(n){let i=o.filter(l=>l.vout===0).sort((l,c)=>c.satoshis>l.satoshis?1:-1);if(i.length===0)throw new St("wallet has no coin at output index 0, which CashTokens requires to create a new certificate category \u2014 send a small amount to yourself first (buildPrepTx)");s.push(i[0]),r+=i[0].satoshis}let a=o.filter(i=>!s.some(l=>l.txid===i.txid&&l.vout===i.vout)).sort((i,l)=>l.satoshis>i.satoshis?1:-1);for(let i of a){if(r>=t)break;s.push(i),r+=i.satoshis}return{inputs:s,totalSats:r}}function rt(e){return{outpointTransactionHash:j(e.txid),outpointIndex:e.vout,sequenceNumber:4294967294,unlockingBytecode:qt()}}function dt(e){return{...rt(e),lockingBytecode:e.lockingBytecode,valueSatoshis:e.satoshis,...e.token?{token:{amount:e.token.amount??0n,category:j(e.token.category),...e.token.nft?{nft:{capability:e.token.nft.capability,commitment:j(e.token.nft.commitment||"")}}:{}}}:{}}}function et(e){return tn(e).length}function Kt({name:e,ownerAddress:t,ownerTokenAddress:n,changeAddress:o,records:s={},utxos:r,feeAddress:a=null,feeSats:i=0n,tldFeeAddress:l=null,tldFeeSats:c=0n,beaconAddress:h=Tt,feeRate:u=1,addressToLockingBytecode:d,cosignUtxo:f=null,cosignTokenAddress:m=null}){if(typeof d!="function")throw new Error("addressToLockingBytecode must be injected (see chain.js)");let g=P(e),p=it("REG",g,s),y=BigInt(i??0n),w=BigInt(c??0n);if(f&&!f.token?.nft)throw new Error("cosignUtxo does not carry a TLD certificate");let x=f?BigInt(f.satoshis):0n,v=f?d(m??n??t):null;if(y>0n&&!a)throw new Error("feeSats set but feeAddress missing");if(w>0n&&!l)throw new Error("tldFeeSats set but tldFeeAddress missing");let b=d(o??t),T=d(n??t),L=d(h),N=y>0n?d(a):null,k=w>0n?d(l):null,C=F+I+y+w+x,E=q(r,C+300n-x),S=0n,A=0n,H=!0;for(let B=0;B<6;B++){let{inputs:K}=E,$=f?[...K,f]:K,R=E.totalSats+x,Z=$[0],X=j(Z.txid),W=(lt,ut)=>{let G=[];return lt&&G.push({lockingBytecode:b,valueSatoshis:ut}),G.push({lockingBytecode:T,valueSatoshis:F,token:{amount:0n,category:X,nft:{capability:"none",commitment:st(g)}}}),G.push({lockingBytecode:pt(p),valueSatoshis:0n}),G.push({lockingBytecode:L,valueSatoshis:I}),k&&G.push({lockingBytecode:k,valueSatoshis:w}),N&&G.push({lockingBytecode:N,valueSatoshis:y}),f&&G.push({lockingBytecode:v,valueSatoshis:x,token:{amount:BigInt(f.token.amount??0),category:j(f.token.category),nft:{capability:f.token.nft.capability,commitment:j(f.token.nft.commitment||"")}}}),{version:2,locktime:0,inputs:$.map(rt),outputs:G}},He=W(!0,1000000n),Me=et(He);S=BigInt(Math.ceil(Me*u));let yt=C+S;if(RK.length){E=lt;continue}throw new J(`not enough funds: need ${yt} sat, have ${R} sat`,{needSats:yt,haveSats:R})}if(A=R-yt,H=A>=U,!H){let lt=W(!1,0n);S=R-C;let ut=BigInt(Math.ceil(et(lt)*u));if(SK.length){E=G;continue}throw new J(`not enough funds: need ${C+ut} sat, have ${R} sat`,{needSats:C+ut,haveSats:R})}A=0n}let Ue=W(H,A),tt={},ot=0;return H&&(tt.change=ot++),tt.certificate=ot++,tt.opReturn=ot++,tt.beacon=ot++,k&&(tt.tldOwnerFee=ot++),N&&(tt.serviceFee=ot++),f&&(tt.cosignCertificate=ot++),{name:g,displayName:Y(g),category:vt(X),transaction:Ue,sourceOutputs:$.map(dt),inputUtxos:$,outputMap:tt,payload:p,costs:{certificateDustSats:F,beaconDustSats:I,serviceFeeSats:y,tldOwnerFeeSats:w,chainFeeSats:S,changeSats:A,totalInputSats:R,netCostSats:y+w+S+I}}}throw new Error("could not converge on a fee \u2014 please retry")}function Et({tld:e,ownerAddress:t,ownerTokenAddress:n,changeAddress:o,records:s={},utxos:r,feeAddress:a=null,feeSats:i=0n,beaconAddress:l=Mt,feeRate:c=1,addressToLockingBytecode:h}){if(typeof h!="function")throw new Error("addressToLockingBytecode must be injected (see chain.js)");let u=ht(e),d=it("TREG",u,s),f=BigInt(i??0n);if(f>0n&&!a)throw new Error("feeSats set but feeAddress missing");let m=h(o??t),g=h(n??t),p=h(l),y=f>0n?h(a):null,w=F+I+f,x=q(r,w+300n),v=0n,b=0n,T=!0;for(let L=0;L<6;L++){let{inputs:N,totalSats:k}=x,C=N[0],E=j(C.txid),S=(Z,X)=>{let W=[];return Z&&W.push({lockingBytecode:m,valueSatoshis:X}),W.push({lockingBytecode:g,valueSatoshis:F,token:{amount:0n,category:E,nft:{capability:"none",commitment:st(u)}}}),W.push({lockingBytecode:pt(d),valueSatoshis:0n}),W.push({lockingBytecode:p,valueSatoshis:I}),y&&W.push({lockingBytecode:y,valueSatoshis:f}),{version:2,locktime:0,inputs:N.map(rt),outputs:W}},A=S(!0,1000000n),H=et(A);v=BigInt(Math.ceil(H*c));let B=w+v;if(kN.length){x=Z;continue}throw new J(`not enough funds: need ${B} sat, have ${k} sat`,{needSats:B,haveSats:k})}if(b=k-B,T=b>=U,!T){let Z=S(!1,0n);v=k-w;let X=BigInt(Math.ceil(et(Z)*c));if(vN.length){x=W;continue}throw new J(`not enough funds: need ${w+X} sat, have ${k} sat`,{needSats:w+X,haveSats:k})}b=0n}let K=S(T,b),$={},R=0;return T&&($.change=R++),$.certificate=R++,$.opReturn=R++,$.beacon=R++,y&&($.serviceFee=R++),{tld:u,category:vt(E),transaction:K,sourceOutputs:N.map(dt),inputUtxos:N,outputMap:$,payload:d,costs:{certificateDustSats:F,beaconDustSats:I,serviceFeeSats:f,chainFeeSats:v,changeSats:b,totalInputSats:k,netCostSats:f+v+I}}}throw new Error("could not converge on a fee \u2014 please retry")}function At({name:e,ownerAddress:t,ownerTokenAddress:n,records:o={},certificateUtxo:s,utxos:r,beaconAddress:a=Tt,feeRate:i=1,addressToLockingBytecode:l}){if(typeof l!="function")throw new Error("addressToLockingBytecode must be injected (see chain.js)");let c=P(e);if(!s?.token?.nft)throw new Error("certificateUtxo does not carry a certificate");let h=vt(st(c));if(s.token.nft.commitment!==h)throw new Error(`certificate commitment mismatch: holds "${de(j(s.token.nft.commitment))}", asked to update "${c}"`);let u=it("UPD",c,o),d=l(t),f=l(n??t),m=l(a),g=F+I,p=q(r,g+400n,{requireGenesis:!1}),y=0n,w=0n,x=!0;for(let v=0;v<6;v++){let b=[s,...p.inputs],T=s.satoshis+p.totalSats,L=(A,H)=>{let B=[];return A&&B.push({lockingBytecode:d,valueSatoshis:H}),B.push({lockingBytecode:f,valueSatoshis:F,token:{amount:0n,category:j(s.token.category),nft:{capability:"none",commitment:st(c)}}}),B.push({lockingBytecode:pt(u),valueSatoshis:0n}),B.push({lockingBytecode:m,valueSatoshis:I}),{version:2,locktime:0,inputs:b.map(rt),outputs:B}},N=et(L(!0,1000000n));y=BigInt(Math.ceil(N*i));let k=g+y;if(Tp.inputs.length){p=A;continue}throw new J(`not enough funds: need ${k} sat, have ${T} sat`,{needSats:k,haveSats:T})}w=T-k,x=w>=U,x||(y=T-g,w=0n);let C=L(x,w),E={},S=0;return x&&(E.change=S++),E.certificate=S++,E.opReturn=S++,E.beacon=S++,{name:c,displayName:Y(c),category:s.token.category,transaction:C,sourceOutputs:b.map(dt),inputUtxos:b,outputMap:E,payload:u,costs:{certificateDustSats:F,beaconDustSats:I,serviceFeeSats:0n,chainFeeSats:y,changeSats:w,totalInputSats:T,netCostSats:y+I}}}throw new Error("could not converge on a fee \u2014 please retry")}function pe({tld:e,ownerAddress:t,ownerTokenAddress:n,records:o={},certificateUtxo:s,utxos:r,beaconAddress:a=Mt,feeRate:i=1,addressToLockingBytecode:l}){if(typeof l!="function")throw new Error("addressToLockingBytecode must be injected (see chain.js)");let c=ht(e);if(!s?.token?.nft)throw new Error("certificateUtxo does not carry a certificate");let h=vt(st(c));if(s.token.nft.commitment!==h)throw new Error(`TLD certificate commitment mismatch: holds "${de(j(s.token.nft.commitment))}", asked to update ".${c}"`);let u=it("TUPD",c,o),d=l(t),f=l(n??t),m=l(a),g=F+I,p=q(r,g+400n,{requireGenesis:!1}),y=0n,w=0n,x=!0;for(let v=0;v<6;v++){let b=[s,...p.inputs],T=s.satoshis+p.totalSats,L=(A,H)=>{let B=[];return A&&B.push({lockingBytecode:d,valueSatoshis:H}),B.push({lockingBytecode:f,valueSatoshis:F,token:{amount:0n,category:j(s.token.category),nft:{capability:"none",commitment:st(c)}}}),B.push({lockingBytecode:pt(u),valueSatoshis:0n}),B.push({lockingBytecode:m,valueSatoshis:I}),{version:2,locktime:0,inputs:b.map(rt),outputs:B}},N=et(L(!0,1000000n));y=BigInt(Math.ceil(N*i));let k=g+y;if(Tp.inputs.length){p=A;continue}throw new J(`not enough funds: need ${k} sat, have ${T} sat`,{needSats:k,haveSats:T})}w=T-k,x=w>=U,x||(y=T-g,w=0n);let C=L(x,w),E={},S=0;return x&&(E.change=S++),E.certificate=S++,E.opReturn=S++,E.beacon=S++,{tld:c,displayName:`.${c}`,category:s.token.category,transaction:C,sourceOutputs:b.map(dt),inputUtxos:b,outputMap:E,payload:u,costs:{certificateDustSats:F,beaconDustSats:I,serviceFeeSats:0n,chainFeeSats:y,changeSats:w,totalInputSats:T,netCostSats:y+I}}}throw new Error("could not converge on a fee \u2014 please retry")}function ft({ownerAddress:e,utxos:t,amountSats:n=10000n,feeRate:o=1,addressToLockingBytecode:s}){let r=s(e),a=q(t,n+400n,{requireGenesis:!1});if(a.inputs.length===0)throw new J("wallet is empty");for(let i=0;i<6;i++){let{inputs:l,totalSats:c}=a,h=(m,g)=>({version:2,locktime:0,inputs:l.map(rt),outputs:[{lockingBytecode:r,valueSatoshis:n},...m?[{lockingBytecode:r,valueSatoshis:g}]:[]]}),u=BigInt(Math.ceil(et(h(!0,1000000n))*o));if(c=U;return{transaction:h(f,d),sourceOutputs:l.map(dt),inputUtxos:l,costs:{chainFeeSats:f?u:c-n}}}throw new Error("could not converge on a fee \u2014 please retry")}function at(e){return e.some(t=>t.vout===0&&!t.token)}function fe({transaction:e,sourceOutputs:t},{broadcast:n=!0,userPrompt:o}={}){return{transaction:e,sourceOutputs:t,broadcast:n,...o?{userPrompt:o}:{}}}import{generateSigningSerializationBCH as rn,hash256 as ye,secp256k1 as an,encodeTransaction as cn,binToHex as Wt}from"@bitauth/libauth";var ge=65;function nt(e,t,n){let o={...e,inputs:e.inputs.map((r,a)=>{let i=t[a],l=n(Wt(i.lockingBytecode));if(!l)throw new Error(`no key for input ${a} \u2014 cannot sign`);let c=rn({inputIndex:a,sourceOutputs:t,transaction:e},{coveredBytecode:i.lockingBytecode,signingSerializationType:Uint8Array.of(ge)}),h=ye(c),u=an.signMessageHashSchnorr(l.privateKey,h);if(typeof u=="string")throw new Error(`signing failed on input ${a}: ${u}`);let d=Uint8Array.from([...u,ge]);return{...r,unlockingBytecode:Uint8Array.from([d.length,...d,l.publicKey.length,...l.publicKey])}})},s=cn(o);return{transaction:o,hex:Wt(s),txid:zt(s)}}function zt(e){return Wt(ye(e).slice().reverse())}function Gt(e){let t=new Uint8Array(e.length/2);for(let n=0;n{let t=new Uint8Array(e.length/2);for(let n=0;n[...new TextEncoder().encode(e)].map(t=>t.toString(16).padStart(2,"0")).join("");function fn(e){let n=String(e).toLowerCase().trim().replace(/^[a-z]+:\/\//,"").split("/")[0].replace(/\.$/,"").split(".").filter(Boolean);if(n.length===0)throw new Error(`invalid name "${e}"`);let o,s;if(n.length===1?(o=n[0],s="bch"):(s=n[n.length-1],o=n[n.length-2]),!ln.test(o)||!be.test(s))throw new Error(`invalid name "${e}" (allowed: ., [a-z0-9-])`);return`${o}.${s}`}var Bt=class{constructor(t,n,o){this.url=t,this.WS=n||globalThis.WebSocket,this.options=o,this.id=0,this.pending=new Map,this.buf=""}connect(){return new Promise((t,n)=>{let o=this.options?new this.WS(this.url,this.options):new this.WS(this.url);this.ws=o;let s=async()=>{try{await this.call("server.version",["bns-web","1.4.2"]),t(this)}catch(i){n(i)}},r=i=>n(new Error("electrum ws error: "+(i?.message||this.url))),a=i=>this._onData(typeof i.data=="string"?i.data:i.data.toString());o.addEventListener?(o.addEventListener("open",s),o.addEventListener("error",r),o.addEventListener("message",a)):(o.onopen=s,o.onerror=r,o.onmessage=a)})}_handle(t){let n=this.pending.get(t.id);n&&(this.pending.delete(t.id),t.error?n.reject(new Error(JSON.stringify(t.error))):n.resolve(t.result))}_onData(t){this.buf+=t;let n;for(;(n=this.buf.indexOf(`
+`))>=0;){let s=this.buf.slice(0,n).trim();if(this.buf=this.buf.slice(n+1),s)try{this._handle(JSON.parse(s))}catch{}}let o=this.buf.trim();if(o)try{let s=JSON.parse(o);this._handle(s),this.buf=""}catch{}}call(t,n=[]){let o=++this.id;return new Promise((s,r)=>{this.pending.set(o,{resolve:s,reject:r}),this.ws.send(JSON.stringify({id:o,method:t,params:n})+`
+`),setTimeout(()=>{this.pending.has(o)&&(this.pending.delete(o),r(new Error(`electrum timeout: ${t}`)))},2e4)})}close(){try{this.ws.close()}catch{}}},gn=e=>typeof e=="string"?{url:e}:e;async function yn(e,t,{directIP:n=!1}={}){let o;for(let s of e){let r=gn(s);try{return await new Bt(r.url,t).connect()}catch(a){o=a}if(n&&r.ip)try{let a=new URL(r.url),i=`${a.protocol}//${r.ip}:${a.port}`;return await new Bt(i,t,{servername:a.hostname}).connect()}catch(a){o=a}}throw o??new Error("no electrum server reachable")}function Se(e){if(!e||!e.startsWith("6a"))return null;let t=pn(e),n=1,o=[];for(;n0?e.height:1/0,o=t.height>0?t.height:1/0;return n-o||e.tx_hash.localeCompare(t.tx_hash)}function Te(e,t){let n=new Set(e.map(i=>i.tx_hash)),o=new Map(e.map(i=>{let l=new Set;for(let c of t.get(i.tx_hash)?.vin??[])c.txid&&c.txid!==i.tx_hash&&n.has(c.txid)&&l.add(c.txid);return[i.tx_hash,l]})),s=new Set,r=[],a=[...e];for(;a.length;){let i=a.findIndex(c=>[...o.get(c.tx_hash)].every(h=>s.has(h)));i<0&&(i=0);let[l]=a.splice(i,1);s.add(l.tx_hash),r.push(l)}return r}function jt(e){let t=e.lastIndexOf(".");return t<0?e:e.slice(t+1)}function wn(e,t,n){let o=n.get(e);return o?o.regHeight>0&&t>0?o.regHeight<=t:!0:!1}async function Pt(e){let t=typeof e.unionHistory=="function"?(await e.unionHistory(we)).history:await e.call("blockchain.scripthash.get_history",[we]);t.sort(ve);let n=new Map;for(let{tx_hash:s}of t)try{n.set(s,await e.call("blockchain.transaction.get",[s,!0]))}catch{}let o=new Map;for(let{tx_hash:s,height:r}of Te(t.filter(a=>n.has(a.tx_hash)),n)){let i=n.get(s).vout||[],l=null;for(let h of i)l=Se(h.scriptPubKey?.hex)??l;if(!l||typeof l.n!="string"||!be.test(l.n))continue;let c=l.n;if(l.o==="TREG"){if(o.has(c))continue;let h=Jt(c),u=i.find(d=>d.tokenData?.nft&&d.tokenData.nft.commitment===h);if(!u)continue;o.set(c,{category:u.tokenData.category,regHeight:r,regTxid:s,mintScriptHex:u.scriptPubKey?.hex??null,records:l.r??{},updates:[{height:r,txid:s,records:l.r??{}}]})}else if(l.o==="TUPD"){let h=o.get(c);if(!h||!i.some(u=>u.tokenData&&u.tokenData.category===h.category))continue;h.records=l.r??{},h.updates.push({height:r,txid:s,records:h.records})}}return o}function xn(e,t){if(!e)return{};let n=e.updates||[];if(!n.length)return e.records||{};let o={};for(let s of n)t>0&&(s.height<=0||s.height>t)||(o=s.records||{});return o}function kn(e,t){let n=xn(e,t);return n.policy==="cosign"||n.hidden===1||n.hidden===!0||n.hidden==="1"}function bn(e,t,n){let o=Jt(n);return(e||[]).some(s=>s.tokenData&&s.tokenData.category===t.category&&s.tokenData.nft&&s.tokenData.nft.commitment===o)}function ke(e){if(!e||!e.scriptPubKey)return null;let t=e.scriptPubKey;return typeof t.address=="string"?t.address:Array.isArray(t.addresses)&&t.addresses.length?t.addresses[0]:null}async function gt({electrum:e=hn,WebSocket:t,client:n,directIP:o=!1,enforceTld:s=!1,cosignGate:r=!0}={}){let a=n??await yn(e,t,{directIP:o});try{let i=null;if(s||r)try{i=await Pt(a)}catch(d){if(s)throw d;i=null}let l=null,c;if(typeof a.unionHistory=="function"){let d=await a.unionHistory(me);c=d.history,l={servers:d.servers,unreachable:d.unreachable,disagreements:d.disagreements}}else c=await a.call("blockchain.scripthash.get_history",[me]);c.sort(ve);let h=new Map;for(let{tx_hash:d}of c)try{h.set(d,await a.call("blockchain.transaction.get",[d,!0]))}catch{}let u=new Map;for(let{tx_hash:d,height:f}of Te(c.filter(m=>h.has(m.tx_hash)),h)){let g=h.get(d).vout||[],p=null;for(let x of g)p=Se(x.scriptPubKey?.hex)??p;if(!p)continue;let y=p.n;if(!un.test(y))continue;let w=dn(y);if(p.o==="REG"){if(u.has(w)||s&&!wn(jt(w),f,i))continue;if(r&&i){let b=i.get(jt(w));if(b&&kn(b,f)&&!bn(g,b,jt(w)))continue}let x=Jt(y),v=g.find(b=>b.tokenData?.nft&&b.tokenData.nft.commitment===x);if(!v)continue;u.set(w,{name:w,category:v.tokenData.category,records:p.r??{},txid:d,height:f,updatedTxid:null,owner:ke(v)})}else if(p.o==="UPD"){let x=u.get(w);if(!x)continue;let v=g.find(b=>b.tokenData&&b.tokenData.category===x.category);if(!v)continue;x.records=p.r??{},x.updatedTxid=d,x.owner=ke(v)??x.owner}}return l&&Object.defineProperty(u,"integrity",{value:l,enumerable:!1}),u}finally{n||a.close()}}async function Sn(e,t={}){let n=fn(e);return(await gt(t)).get(n)??null}var O={network:"chipnet",addressPrefix:"bchtest",selfHosted:null,get electrum(){return this.selfHosted?[this.selfHosted,...bt]:bt},beacon:"bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl",serviceFee:{address:"bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu",sats:10000n},feeRate:1,wizardConnect:{dappName:"Silent Mode",dappIcon:"https://silentmode.st/icon.png"},faucets:[{name:"tbch.googol.cash",url:"https://tbch.googol.cash/"},{name:"chipnet.bch.ninja faucet",url:"https://chipnet.bch.ninja/faucet"}]};function vn(e){let t=r=>(Number(r)/1e8).toFixed(8).replace(/0+$/,"").replace(/\.$/,""),n=[],o=BigInt(e.tldOwnerFeeSats??0n),s=BigInt(e.serviceFeeSats??0n);return o>0n?(n.push({label:"Service fee \u2014 TLD owner (90%)",sats:o,bch:t(o),note:"goes to whoever holds this TLD's certificate on chain"}),n.push({label:"Service fee \u2014 platform (10%)",sats:s,bch:t(s),note:"covers the resolver + gateway + indexer"})):n.push({label:"Service fee",sats:s,bch:t(s)}),n.push({label:"Network fee",sats:e.chainFeeSats,bch:t(e.chainFeeSats)}),n.push({label:"Discovery beacon",sats:e.beaconDustSats,bch:t(e.beaconDustSats)}),n.push({label:"Held in the certificate",sats:e.certificateDustSats,bch:t(e.certificateDustSats),note:"stays yours \u2014 it lives in the certificate you now own"}),n.push({label:"Total",sats:e.netCostSats,bch:t(e.netCostSats),total:!0}),n}import{hexToBin as De,binToHex as ct,decodeTransaction as On,lockingBytecodeToCashAddress as Nn,sha256 as Dn,utf8ToBin as $n,binToBase64 as Rn}from"@bitauth/libauth";import{encodeTransaction as Xt,decodeTransaction as Ee,generateSigningSerializationBCH as Ae,hash256 as Vt,hash160 as Tn,secp256k1 as Be,hexToBin as Q,binToHex as D,utf8ToBin as _e}from"@bitauth/libauth";var En=65,Yt=195,Qt=(e,t)=>({outpointTransactionHash:Q(e.txid),outpointIndex:e.vout,sequenceNumber:4294967294,unlockingBytecode:t??qt()}),wt=e=>e?{amount:BigInt(e.amount??0),category:Q(e.category),...e.nft?{nft:{capability:e.nft.capability,commitment:Q(e.nft.commitment||"")}}:{}}:void 0,Zt=e=>({lockingBytecode:e.lockingBytecode instanceof Uint8Array?D(e.lockingBytecode):String(e.lockingBytecode),valueSatoshis:String(e.valueSatoshis),...e.token?{token:{category:e.token.category instanceof Uint8Array?D(e.token.category):String(e.token.category),amount:String(e.token.amount??0),...e.token.nft?{nft:{capability:e.token.nft.capability,commitment:e.token.nft.commitment instanceof Uint8Array?D(e.token.nft.commitment):String(e.token.nft.commitment||"")}}:{}}}:{}}),te=e=>({lockingBytecode:Q(e.lockingBytecode),valueSatoshis:BigInt(e.valueSatoshis??e.satoshis),...e.token?{token:wt(e.token)}:{}}),Ie=(e,t)=>({...Qt(e,t),lockingBytecode:e.lockingBytecode instanceof Uint8Array?e.lockingBytecode:Q(e.lockingBytecode),valueSatoshis:BigInt(e.satoshis),...e.token?{token:wt(e.token)}:{}});function Oe(e,t,n,o,s){let r=t[n],a=Ae({inputIndex:n,sourceOutputs:t,transaction:e},{coveredBytecode:r.lockingBytecode,signingSerializationType:Uint8Array.of(s)}),i=Vt(a),l=Be.signMessageHashSchnorr(o.privateKey,i);if(typeof l=="string")throw new Error(`signing failed on input ${n}: ${l}`);let c=Uint8Array.from([...l,s]);return Uint8Array.from([c.length,...c,o.publicKey.length,...o.publicKey])}function _t(e,t,n,{sighash:o=En,skip:s=[]}={}){let r={...e,inputs:e.inputs.map((i,l)=>{if(s.includes(l))return i;let c=n(D(t[l].lockingBytecode));return c?{...i,unlockingBytecode:Oe(e,t,l,c,o)}:i})},a=Xt(r);return{transaction:r,hex:D(a),txid:D(Vt(a).slice().reverse())}}function An({name:e,certificateUtxo:t,priceSats:n,sellerAddress:o,addressToLockingBytecode:s,keyFor:r}){let a=P(e),i=BigInt(n);if(iUint8Array.from([118,169,20,...Tn(e),136,172]);function Ne(e,t={}){let n=o=>({ok:!1,error:o});try{if(!e||e.v!==1)return n("unsupported listing version");let o=P(e.name),s=Ee(Q(e.partial_tx));if(typeof s=="string")return n("partial_tx does not decode: "+s);if(s.inputs.length!==1||s.outputs.length<1)return n("partial_tx must have exactly one input and a payment output");let r=e.source_output;if(!r?.token?.nft)return n("source_output carries no certificate");if(D(s.inputs[0].outpointTransactionHash)!==r.txid||s.inputs[0].outpointIndex!==r.vout)return n("input does not spend source_output");if(r.token.nft.commitment!==D(_e(o)))return n("certificate commitment does not match the name");if(t.category&&r.token.category!==t.category)return n("certificate category does not match the registry");let a=s.outputs[0].valueSatoshis;if(aXt(L).length;for(let L=0;L<6;L++){let{inputs:N,totalSats:k}=b,C=(K,$)=>({version:2,locktime:0,inputs:[d,...N.map(R=>Qt(R))],outputs:[m,{lockingBytecode:y,valueSatoshis:F,token:wt(u.token)},{lockingBytecode:pt(x),valueSatoshis:0n},{lockingBytecode:w,valueSatoshis:I},...K?[{lockingBytecode:p,valueSatoshis:$}]:[]]}),E=BigInt(Math.ceil(T(C(!0,1000000n))*a)),S=v+E;if(kN.length){b=K;continue}let $=new Error(`not enough funds: need ${S} sat, have ${k} sat`);throw $.needSats=S,$.haveSats=k,$}let A=k-S,H=A>=U;H||(A=0n);let B=C(H,A);return{name:c,transaction:B,sourceOutputs:[f,...N.map(K=>Ie(K))],sellerInputIndex:0,costs:{priceSats:g,beaconDustSats:I,certificateDustSats:F,chainFeeSats:H?E:k-v,changeSats:A,totalInputSats:k}}}throw new Error("could not converge on a fee \u2014 please retry")}var Fn=90n,Ln=100n;async function $e(e,t,n=O.addressPrefix??"bchtest"){let s=(await Pt(e)).get(String(t).toLowerCase());if(!s?.mintScriptHex)return null;try{let r=De(s.mintScriptHex),a=Nn({bytecode:r,prefix:n,tokenSupport:!1});return typeof a=="string"?null:a.address}catch{return null}}function Re(e){let t=BigInt(e??0n),n=t*Fn/Ln,o=t-n;return{tldOwnerFeeSats:n,platformFeeSats:o}}var Cn=/silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/;async function Hn({electrum:e=O.electrum,WebSocket:t,beaconOk:n=!1}={}){let o=n?e:e.filter(s=>!Cn.test(typeof s=="string"?s:s.url||""));return ce(o,t)}async function It(e,t){let n=P(t),o=Y(n),r=(await gt({client:e})).get(o)??null;return{name:o,rawName:n,available:!r,entry:r,recordBudgetBytes:Ut(n)}}async function Ot(e,{name:t,ownerAddress:n,records:o={},utxos:s,serviceFee:r=O.serviceFee,feeRate:a=O.feeRate,cosignUtxo:i=null,cosignTokenAddress:l=null}){let c=s??await z(e,[n]),h=P(t),u=h.indexOf("."),d=u<0?"bch":h.slice(u+1),f=BigInt(r.sats??0n),m=null,g=0n,p=f;if(f>0n){try{m=await $e(e,d)}catch{m=null}if(m&&m!==n){let y=Re(f);g=y.tldOwnerFeeSats,p=y.platformFeeSats}}return Kt({name:t,ownerAddress:n,ownerTokenAddress:M(n),records:o,utxos:c,feeAddress:p>0n?r.address:null,feeSats:p,tldFeeAddress:g>0n?m:null,tldFeeSats:g,beaconAddress:O.beacon,feeRate:a,addressToLockingBytecode:_,cosignUtxo:i,cosignTokenAddress:l})}function Fe(e,{tld:t,category:n}){let o=[...new TextEncoder().encode(String(t).toLowerCase())].map(s=>s.toString(16).padStart(2,"0")).join("");return e.find(s=>s.token?.nft&&(!n||s.token.category===n)&&s.token.nft.commitment===o)??null}async function Le(e,{wallet:t,utxos:n,feeRate:o,onProgress:s}){if(at(n))return{utxos:n,prepTxid:null};s("preparing-wallet");let r=ft({ownerAddress:t.address,utxos:n,feeRate:o,addressToLockingBytecode:_}),a=nt(r.transaction,r.sourceOutputs,c=>t.keyFor(c)),i=await V(e,a.hex);return s("prepared",{txid:i}),{utxos:a.transaction.outputs.slice(0,2).map((c,h)=>({txid:i,vout:h,satoshis:c.valueSatoshis,address:t.address,lockingBytecode:_(t.address)})),prepTxid:i}}async function Mn(e,{wallet:t,name:n,records:o={},serviceFee:s=O.serviceFee,feeRate:r=O.feeRate,tldCert:a,onProgress:i=()=>{}}){if(!a?.utxo?.token?.nft||!a.owner)throw new Error("tldCert must be the /api/tld-cert response ({ owner, utxo })");i("checking-availability");let l=await It(e,n);if(!l.available)throw new Error(`"${l.name}" is already registered`);i("loading-coins");let c=await z(e,t.watchedAddresses);if(c.filter(p=>!p.token).reduce((p,y)=>p+y.satoshis,0n)===0n)throw new Error("wallet is empty \u2014 fund the address first");let u=null;({utxos:c,prepTxid:u}=await Le(e,{wallet:t,utxos:c,feeRate:r,onProgress:i})),i("building");let d=await Ot(e,{name:n,ownerAddress:t.address,records:o,utxos:c,serviceFee:s,feeRate:r,cosignUtxo:{...a.utxo,address:a.owner},cosignTokenAddress:M(a.owner)}),f=d.transaction.inputs.length-1;i("signing");let m={...d.transaction,inputs:d.transaction.inputs.map((p,y)=>y===f?{...p,unlockingBytecode:new Uint8Array}:p)},g=_t(m,d.sourceOutputs,p=>t.keyFor(p),{skip:[f]});return{name:d.name,displayName:d.displayName,category:d.category,tx:g.hex,sourceOutputs:d.sourceOutputs.map(Zt),cosignInputIndex:f,costs:d.costs,prepTxid:u}}function Un(e,t){let n=On(De(String(e.tx||"")));if(typeof n=="string")throw new Error("invalid transaction: "+n);let o=(e.sourceOutputs||[]).map(te);if(o.length!==n.inputs.length)throw new Error("sourceOutputs do not match the transaction inputs");let s=[],r=0;n.inputs.forEach((i,l)=>{let c=o[l];if(!t(ct(c.lockingBytecode))){s.push(l);return}if(c.token?.nft&&!n.outputs.some(u=>u.token&&ct(u.token.category)===ct(c.token.category)&&ct(u.token.nft?.commitment??new Uint8Array)===ct(c.token.nft.commitment??new Uint8Array)&&ct(u.lockingBytecode)===ct(c.lockingBytecode)&&u.valueSatoshis>=c.valueSatoshis))throw new Error("refusing to sign: the request would not return your TLD certificate to you");r+=1});let a=_t(n,o,t,{skip:s});return{hex:a.hex,txid:a.txid,signedInputs:r}}async function qn(e,{wallet:t,name:n,records:o={},serviceFee:s=O.serviceFee,feeRate:r=O.feeRate,cosign:a=null,onProgress:i=()=>{}}){i("checking-availability");let l=await It(e,n);if(!l.available)throw new Error(`"${l.name}" is already registered`);i("loading-coins");let c=await z(e,t.watchedAddresses);if(c.filter(p=>!p.token).reduce((p,y)=>p+y.satoshis,0n)===0n)throw new Error("wallet is empty \u2014 fund the address first");let u=null;if(a&&(u=Fe(c,a),!u))throw new Error(`this wallet does not hold the certificate for .${a.tld} \u2014 only the TLD owner can co-sign`);let d=null;({utxos:c,prepTxid:d}=await Le(e,{wallet:t,utxos:c,feeRate:r,onProgress:i})),i("building");let f=await Ot(e,{name:n,ownerAddress:t.address,records:o,utxos:c,serviceFee:s,feeRate:r,cosignUtxo:u,cosignTokenAddress:u?M(t.address):null});i("signing");let m=nt(f.transaction,f.sourceOutputs,p=>t.keyFor(p));i("broadcasting");let g=await V(e,m.hex);return i("registered",{txid:g,category:f.category}),{name:f.name,displayName:f.displayName,category:f.category,txid:g,prepTxid:d,ownerAddress:t.address,certificateAddress:M(t.address),costs:f.costs,hex:m.hex}}async function Kn(e,{wallet:t,tld:n,records:o={},serviceFee:s={address:null,sats:0n},feeRate:r=O.feeRate,tldListUrl:a=null,onProgress:i=()=>{}}){let l=ht(n);if(i("checking-availability"),a)try{let g=await fetch(a);if(g.ok){let p=await g.json();if(Array.isArray(p.tlds)&&p.tlds.some(w=>(w.tld||w)===l))throw new Error(`".${l}" is already registered as a TLD`)}}catch(g){if(/already registered/.test(g.message))throw g}i("loading-coins");let c=await z(e,t.watchedAddresses);if(c.filter(g=>!g.token).reduce((g,p)=>g+p.satoshis,0n)===0n)throw new Error("wallet is empty \u2014 fund the address first");let u=null;if(!at(c)){i("preparing-wallet");let g=ft({ownerAddress:t.address,utxos:c,feeRate:r,addressToLockingBytecode:_}),p=nt(g.transaction,g.sourceOutputs,y=>t.keyFor(y));u=await V(e,p.hex),i("prepared",{txid:u}),c=[{txid:u,vout:0,satoshis:p.transaction.outputs[0].valueSatoshis,address:t.address,lockingBytecode:_(t.address)},...p.transaction.outputs.length>1?[{txid:u,vout:1,satoshis:p.transaction.outputs[1].valueSatoshis,address:t.address,lockingBytecode:_(t.address)}]:[]]}i("building");let d=Et({tld:l,ownerAddress:t.address,ownerTokenAddress:M(t.address),changeAddress:t.address,records:o,utxos:c,feeAddress:s.address||null,feeSats:BigInt(s.sats??0n),feeRate:r,addressToLockingBytecode:_});i("signing");let f=nt(d.transaction,d.sourceOutputs,g=>t.keyFor(g));i("broadcasting");let m=await V(e,f.hex);return i("registered",{txid:m,category:d.category}),{tld:d.tld,category:d.category,txid:m,prepTxid:u,ownerAddress:t.address,certificateAddress:M(t.address),costs:d.costs,hex:f.hex}}async function Wn(e,{session:t,tld:n,records:o={},serviceFee:s={address:null,sats:0n},feeRate:r=O.feeRate,tldListUrl:a=null,onProgress:i=()=>{}}){let l=ht(n);if(i("checking-availability"),a)try{let p=await fetch(a);if(p.ok){let y=await p.json();if(Array.isArray(y.tlds)&&y.tlds.some(x=>(x.tld||x)===l))throw new Error(`".${l}" is already registered as a TLD`)}}catch(p){if(/already registered/.test(p.message))throw p}i("reading-wallet");let c=await t.getAddresses();if(!c.length)throw new Error("the wallet exposed no addresses");let h=c[0];i("loading-coins");let u=await z(e,c);if(u.filter(p=>!p.token).reduce((p,y)=>p+y.satoshis,0n)===0n)throw new Error("the connected wallet has no spendable coins");let d=null;if(!at(u)){i("preparing-wallet");let p=ft({ownerAddress:h,utxos:u,feeRate:r,addressToLockingBytecode:_}),y=await t.signTransaction(p,{userPrompt:"One-time setup so your wallet can create a TLD certificate"});d=await xt(e,t,y,i),i("prepared",{txid:d}),u=[{txid:d,vout:0,satoshis:p.transaction.outputs[0].valueSatoshis,address:h,lockingBytecode:_(h)},...u.filter(w=>!w.token&&!p.inputUtxos.some(x=>x.txid===w.txid&&x.vout===w.vout))]}i("building");let f=Et({tld:l,ownerAddress:h,ownerTokenAddress:M(h),changeAddress:h,records:o,utxos:u,feeAddress:s.address||null,feeSats:BigInt(s.sats??0n),feeRate:r,addressToLockingBytecode:_});i("awaiting-approval");let m=await t.signTransaction(f,{userPrompt:`Register .${l} \u2014 the TLD certificate is minted to your own address`}),g=await xt(e,t,m,i);return i("registered",{txid:g,category:f.category}),{tld:f.tld,category:f.category,txid:g,prepTxid:d,ownerAddress:h,certificateAddress:M(h),costs:f.costs}}async function xt(e,t,n,o){let s=n.signedTransactionHash??Gt(n.signedTransaction);if(t.walletBroadcasts)return s;o("broadcasting");try{return await V(e,n.signedTransaction)}catch(r){let a=String(r?.message??r);if(/already|duplicate|txn-already|in mempool/i.test(a))return s;throw r}}async function zn(e,{session:t,name:n,ownerAddress:o,records:s={},serviceFee:r=O.serviceFee,feeRate:a=O.feeRate,onProgress:i=()=>{}}){i("checking-availability");let l=await It(e,n);if(!l.available)throw new Error(`"${l.name}" is already registered`);i("reading-wallet");let c=await t.getAddresses();if(!c.length)throw new Error("the wallet exposed no addresses");let h=o??c[0];i("loading-coins");let u=await z(e,c);if(!u.some(g=>!g.token))throw new Error("the connected wallet has no spendable coins");if(!at(u)){i("preparing-wallet");let g=ft({ownerAddress:h,utxos:u,feeRate:a,addressToLockingBytecode:_}),p=await t.signTransaction(g,{userPrompt:"One-time setup so your wallet can create a name certificate"}),y=await xt(e,t,p,i);i("prepared",{txid:y}),u=[{txid:y,vout:0,satoshis:g.transaction.outputs[0].valueSatoshis,address:h,lockingBytecode:_(h)},...u.filter(w=>!w.token&&!g.inputUtxos.some(x=>x.txid===w.txid&&x.vout===w.vout))]}i("building");let d=await Ot(e,{name:n,ownerAddress:h,records:s,utxos:u,serviceFee:r,feeRate:a});i("awaiting-approval");let f=await t.signTransaction(d,{userPrompt:`Register ${d.displayName} \u2014 the certificate is minted to your own address`}),m=await xt(e,t,f,i);return i("registered",{txid:m,category:d.category}),{name:d.name,displayName:d.displayName,category:d.category,txid:m,ownerAddress:h,certificateAddress:M(h),costs:d.costs}}async function Gn(e,{addresses:t,category:n,rawName:o}){let s=await z(e,t),r=[...new TextEncoder().encode(o)].map(a=>a.toString(16).padStart(2,"0")).join("");return s.find(a=>a.token?.category===n&&a.token?.nft?.commitment===r)??null}async function jn(e,{wallet:t,name:n,records:o,feeRate:s=O.feeRate,onProgress:r=()=>{}}){r("resolving");let a=P(n),l=(await gt({client:e})).get(Y(a));if(!l)throw new Error(`"${Y(a)}" is not registered`);r("loading-coins");let c=await z(e,t.watchedAddresses),h=c.find(m=>m.token?.category===l.category&&m.token?.nft);if(!h)throw new Error(`this wallet does not hold the certificate for "${Y(a)}" \u2014 only the owner can change its records`);r("building");let u=At({name:n,ownerAddress:t.address,ownerTokenAddress:M(t.address),records:o,certificateUtxo:h,utxos:c.filter(m=>!m.token),beaconAddress:O.beacon,feeRate:s,addressToLockingBytecode:_});r("signing");let d=nt(u.transaction,u.sourceOutputs,m=>t.keyFor(m));r("broadcasting");let f=await V(e,d.hex);return r("updated",{txid:f}),{name:u.name,displayName:u.displayName,category:u.category,txid:f,records:o,costs:u.costs}}async function Jn(e,{session:t,name:n,records:o,feeRate:s=O.feeRate,onProgress:r=()=>{}}){r("resolving");let a=P(n),l=(await gt({client:e})).get(Y(a));if(!l)throw new Error(`"${Y(a)}" is not registered`);r("reading-wallet");let c=await t.getAddresses(),h=await z(e,c),u=h.find(g=>g.token?.category===l.category&&g.token?.nft);if(!u)throw new Error(`this wallet does not hold the certificate for "${Y(a)}" \u2014 only the owner can change its records`);r("building");let d=At({name:n,ownerAddress:u.address,ownerTokenAddress:M(u.address),records:o,certificateUtxo:u,utxos:h.filter(g=>!g.token),beaconAddress:O.beacon,feeRate:s,addressToLockingBytecode:_});r("awaiting-approval");let f=await t.signTransaction(d,{userPrompt:`Point ${d.displayName} somewhere new`}),m=await xt(e,t,f,r);return r("updated",{txid:m}),{name:d.name,displayName:d.displayName,category:d.category,txid:m,records:o,costs:d.costs}}async function Pn(e,{wallet:t,tld:n,records:o,category:s,feeRate:r=O.feeRate,onProgress:a=()=>{}}){if(!s)throw new Error("setTldRecords requires the TLD certificate's category id");a("loading-coins");let i=await z(e,t.watchedAddresses),l=i.find(d=>d.token?.category===s&&d.token?.nft);if(!l)throw new Error(`this wallet does not hold the TLD certificate for ".${n}" \u2014 only the owner can change its policy`);a("building");let c=pe({tld:n,ownerAddress:t.address,ownerTokenAddress:M(t.address),records:o,certificateUtxo:l,utxos:i.filter(d=>!d.token),feeRate:r,addressToLockingBytecode:_});a("signing");let h=nt(c.transaction,c.sourceOutputs,d=>t.keyFor(d));a("broadcasting");let u=await V(e,h.hex);return a("updated",{txid:u}),{tld:c.tld,displayName:c.displayName,category:c.category,txid:u,records:o,costs:c.costs}}async function Yn(e,t){if(!t||t.v!==1)throw new Error("manifest.v must be 1");if(!t.name)throw new Error("manifest.name is required");if(typeof t.seq!="number"||t.seq<0)throw new Error("manifest.seq must be a non-negative integer");let n={...t};delete n.sig;let o=JSON.stringify(ee(n)),s=Dn.hash($n(o)),r=e.signMessage(s);return{...t,sig:Rn(r)}}function ee(e){if(e!=null){if(Array.isArray(e))return e.map(ee).filter(t=>t!==void 0);if(typeof e=="object"){let t={};for(let n of Object.keys(e).sort()){let o=ee(e[n]);o!==void 0&&(t[n]=o)}return t}return e}}async function Xn(e,t){let n=await z(e,t),o=n.filter(s=>!s.token);return{sats:o.reduce((s,r)=>s+r.satoshis,0n),confirmedSats:o.filter(s=>s.height>0).reduce((s,r)=>s+r.satoshis,0n),coins:o.length,certificates:n.filter(s=>s.token?.nft),hasGenesisInput:at(n)}}import{hash160 as Vn,lockingBytecodeToCashAddress as Qn,binToHex as $o}from"@bitauth/libauth";function Ce(e){return Uint8Array.from([118,169,20,...Vn(e),136,172])}function Zn(e,t="bchtest",n=!1){let o=Qn({bytecode:Ce(e),prefix:t,tokenSupport:n});if(typeof o=="string")throw new Error(`address encoding failed: ${o}`);return o.address}function to(e){return e?{ready:!0,approvals:1,explanation:"Your wallet can mint the certificate directly."}:{ready:!1,approvals:2,explanation:"Your wallet needs one small setup transaction to itself before it can create a certificate \u2014 a rule of the Bitcoin Cash token system, not a fee we charge. You will be asked to approve it, then the registration. This happens only once."}}import{binToHex as Jo,hexToBin as Po}from"@bitauth/libauth";import{stringify as Xo}from"@bitauth/libauth";export{Rt as BuiltInWallet,mt as CHIPNET_PREFIX,J as InsufficientFundsError,St as NoGenesisInputError,O as REGISTRAR,Mt as TLD_BEACON,_ as addressToLockingBytecode,Ze as addressToScripthash,Jo as binToHex,V as broadcast,Mn as buildCosignRequest,gt as buildIndex,An as buildListing,ft as buildPrepTx,Kt as buildRegistrationTx,Et as buildTldRegistrationTx,At as buildUpdateTx,It as checkAvailability,In as completeSale,Hn as connect,to as describeGenesisRequirement,Y as displayName,Gn as findCertificateUtxo,Fe as findTldCertificateUtxo,$e as findTldOwnerAddress,Xn as getBalance,ue as getUtxos,z as getUtxosForAddresses,at as hasGenesisInput,Po as hexToBin,P as normalizeName,ht as normalizeTld,Ut as payloadBudget,vn as priceSummary,Zn as pubkeyToAddress,Ce as pubkeyToLockingBytecode,Ot as quoteRegistration,Kn as registerTldWithBuiltInWallet,Wn as registerTldWithExternalWallet,qn as registerWithBuiltInWallet,zn as registerWithExternalWallet,Sn as resolveName,jn as setRecordsWithBuiltInWallet,Jn as setRecordsWithExternalWallet,Pn as setTldRecordsWithBuiltInWallet,Un as signCosignRequest,_t as signInputs,Yn as signRecordsManifest,nt as signTransaction,te as sourceOutputFromJson,Zt as sourceOutputToJson,Re as splitServiceFee,Xo as stringify,M as toTokenAddress,fe as toWcTransactionObject,zt as txidOf,Gt as txidOfHex,Ne as verifyListing};
diff --git a/extensions/js/session.js b/extensions/js/session.js
new file mode 100644
index 00000000..6cb83b42
--- /dev/null
+++ b/extensions/js/session.js
@@ -0,0 +1,141 @@
+// Sirius.X persistent sign-in ("stay signed in on this device").
+//
+// Problem: every visit asked for the seed phrase, the password or the PIN
+// again. A dashboard people come back to daily cannot work like that.
+//
+// Model: after one successful sign-in the wallet's recovery phrase is
+// encrypted with a random AES-GCM key that lives ONLY inside IndexedDB as a
+// non-extractable CryptoKey (the browser can use it, scripts cannot read
+// its bytes). The ciphertext sits in localStorage. On the next visit the
+// dashboard decrypts silently and the user is in — no prompt. Signing out,
+// or turning "stay signed in" off in Settings, destroys both halves.
+//
+// Trust boundary, stated honestly: this is a device session, like a
+// "remember me" cookie. Anyone who can run scripts in this browser profile
+// can use the wallet while the session exists. The password-encrypted
+// wallet blob (bns.wallet.v1) and the optional PIN escrow are unchanged and
+// still the durable secrets. Users who want a prompt before every payment
+// can keep that in Settings ("Ask for PIN before payments").
+//
+// window.siriusSession = { isEnabled, setEnabled, remember, restore, forget,
+// requirePinForPayments, setRequirePinForPayments }
+
+(() => {
+ const DB = "sirius-session";
+ const STORE = "keys";
+ const KEY_ID = "device";
+ const BLOB = "siriusSessionBlob";
+ const ENABLED = "siriusStaySignedIn"; // "0" = off; anything else = on (default on)
+ const REQUIRE_PIN = "siriusRequirePin"; // "1" = ask PIN before payments (default off)
+
+ const enc = new TextEncoder();
+ const dec = new TextDecoder();
+ const b64 = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes)));
+ const unb64 = (str) => Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
+
+ function openDb() {
+ return new Promise((resolve, reject) => {
+ if (!("indexedDB" in window)) return reject(new Error("IndexedDB unavailable"));
+ const req = indexedDB.open(DB, 1);
+ req.onupgradeneeded = () => { req.result.createObjectStore(STORE); };
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error || new Error("IndexedDB open failed"));
+ });
+ }
+ function idbGet(db, key) {
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(STORE, "readonly");
+ const req = tx.objectStore(STORE).get(key);
+ req.onsuccess = () => resolve(req.result || null);
+ req.onerror = () => reject(req.error);
+ });
+ }
+ function idbPut(db, key, value) {
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(STORE, "readwrite");
+ tx.objectStore(STORE).put(value, key);
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+ }
+ function idbDel(db, key) {
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(STORE, "readwrite");
+ tx.objectStore(STORE).delete(key);
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+ }
+
+ async function deviceKey(create) {
+ const db = await openDb();
+ try {
+ let key = await idbGet(db, KEY_ID);
+ if (!key && create) {
+ key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
+ await idbPut(db, KEY_ID, key);
+ }
+ return key;
+ } finally { db.close(); }
+ }
+
+ function isEnabled() {
+ try { return localStorage.getItem(ENABLED) !== "0"; } catch { return true; }
+ }
+ function setEnabled(on) {
+ try { localStorage.setItem(ENABLED, on ? "1" : "0"); } catch {}
+ if (!on) forget();
+ }
+ function requirePinForPayments() {
+ try { return localStorage.getItem(REQUIRE_PIN) === "1"; } catch { return false; }
+ }
+ function setRequirePinForPayments(on) {
+ try { localStorage.setItem(REQUIRE_PIN, on ? "1" : "0"); } catch {}
+ }
+ function hasSession() {
+ try { return !!localStorage.getItem(BLOB); } catch { return false; }
+ }
+
+ // Encrypt and store the recovery phrase for silent restore. `meta` may
+ // carry { accountPath } for wallets derived on a non-default path.
+ async function remember(mnemonic, meta = {}) {
+ if (!isEnabled() || !mnemonic) return false;
+ try {
+ const key = await deviceKey(true);
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(mnemonic));
+ localStorage.setItem(BLOB, JSON.stringify({ v: 1, iv: b64(iv), ct: b64(ct), at: Date.now(), path: meta.accountPath || null }));
+ return true;
+ } catch { return false; }
+ }
+
+ // Silent restore. Resolves { mnemonic, accountPath } or null. Never throws.
+ async function restore() {
+ if (!isEnabled()) return null;
+ let raw = null;
+ try { raw = localStorage.getItem(BLOB); } catch {}
+ if (!raw) return null;
+ try {
+ const blob = JSON.parse(raw);
+ const key = await deviceKey(false);
+ if (!key) { forget(); return null; }
+ const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: unb64(blob.iv) }, key, unb64(blob.ct));
+ return { mnemonic: dec.decode(pt), accountPath: blob.path || null };
+ } catch {
+ // Key rotated / storage cleared / tampered — drop the stale blob.
+ forget();
+ return null;
+ }
+ }
+
+ async function forget() {
+ try { localStorage.removeItem(BLOB); } catch {}
+ try { const db = await openDb(); try { await idbDel(db, KEY_ID); } finally { db.close(); } } catch {}
+ }
+
+ window.siriusSession = {
+ isEnabled, setEnabled, hasSession,
+ remember, restore, forget,
+ requirePinForPayments, setRequirePinForPayments,
+ };
+})();
diff --git a/extensions/publish/index.html b/extensions/publish/index.html
new file mode 100644
index 00000000..fc3e8cab
--- /dev/null
+++ b/extensions/publish/index.html
@@ -0,0 +1,239 @@
+
+
+
+
+
+theseus.x — Publish an extension
+
+
+
+
+
+
+
+ theseus.x
+ Home
+ Plug-ins
+ Extensions
+ Publish
+
+
+
Publish an extension
+
Three steps, all in this tab: unlock the wallet that holds your BCDN name, choose the packaged
+ extension, sign. The gateway checks the signature against the name's on-chain owner, stores the package on
+ Sia and updates the catalog . Your recovery phrase never leaves this page.
+
+
+
1 Wallet ✓ unlocked
+
+
+
+
+
+
2 Publisher name ✓ you own it
+
A BCDN name you own (e.g. you.bch) — it becomes the publisher of record
+
Check ownership
+
+
+
+
+
3 Package ✓ looks valid
+
Gzipped tar of the extension folder (tar -czf my-ext-1.0.0.tar.gz -C my-ext ., max 8 MB)
+
+
+ id
+ name
+ version
+ main
+ files
+ sha256
+
+
+
+
+
+
4 Sign and publish
+
Two signatures are made with your wallet key: one authorises this upload, the other is stored in the
+ extension's channel so every Theseus can verify the package came from your name.
+
Sign & publish
+
+
+
Install it from Theseus › Settings › Extensions › Community. New versions: come back with a higher version number.
+
+
+
+
What the gateway enforces. The signature must recover to the name's current NFT owner ·
+ addon.json at the package root with matching id/version and a main script ·
+ an id belongs to the first name that publishes it · each new version must be higher than the last · 8 MB cap.
+
+
+
+
+
+
diff --git a/index.html b/index.html
index f9c00b4f..087ca829 100644
--- a/index.html
+++ b/index.html
@@ -103,6 +103,7 @@
Features
Download
Verify
+ Plug-ins
Extensions
sirius.x
Get Theseus
diff --git a/plug-ins/index.html b/plug-ins/index.html
new file mode 100644
index 00000000..ef8e0241
--- /dev/null
+++ b/plug-ins/index.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+theseus.x — Plug-ins
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ theseus.x
+
+ Home
+ Download
+ Plug-ins
+ Extensions
+ sirius.x
+
+
+
+
+
+
+
+ Aegis — the wallet
+ Multi-chain wallet derived from your Theseus vault (BCH, BTC, TRX, ETH, SOL, SC, DGB) with a
+ dApp bridge for the pages you visit. Bundled with every Theseus build and updated over a signed channel
+ between releases; the panel's own Stage → Restart flow applies an update without reinstalling Theseus.
+
+
🛡️ Aegis current 0.6.31
+
Vault-derived keys, per-chain accounts, imported keys, message signing, dApp permissions per site.
+
Plug-in · Settings › Plug-ins · signed versions on the channel
+
+
Signed update manifest — Ed25519 by the Silent Mode operator, SHA-256 verified before install
+
+
+
sha256 —
+
+
+
+
+
+ Ariadne's Thread — the system-wide resolver
+ Makes BCDN names resolve for every program on the machine, not only Theseus: a local
+ DNS resolver plus gateway, installed by Theseus's setup by default (opt-out) and switchable from
+ Settings › Plug-ins . The same resolver ships standalone for Windows and as Ariadne for Android.
+
+
🧵 Ariadne's Thread current 0.1.3
+
Windows service: answers A/AAAA for registered names, forwards everything else to your upstream
+ resolver, serves Sia-hosted content through a local gateway with a machine-local CA. Answers MX/TXT/NS from
+ owner-signed DNS records.
+
Plug-in · Settings › Plug-ins · Turn on / off · Install / Update / Uninstall from inside Theseus
+
+
+
Hashes from the releases manifest — verify before running
+
Installer —
+
APK —
+
+
+
+
+
+
+ Where the line is
+ Plug-ins are first-party: Silent Mode builds them, signs them with the operator key, and Theseus treats
+ them as part of itself (own Settings section, own update flow). Extensions are everyone else's: published by
+ any BCDN name holder, signed with that name's wallet, listed on theseus.x/extensions
+ and installed from Settings › Extensions . Both are verified on your device before a byte runs.
+ Want to build one? Start on the
Extensions page — the same add-on
+ format powers both, and publishing takes a name and a signature, nothing else.
+
+
+
+
+
+
+
+
+