site/js/bns-register.js

8 lines
60 KiB
JavaScript
Raw Normal View History

import{generateBip39Mnemonic as Bn,deriveSeedFromBip39Mnemonic as Ae,deriveHdPrivateNodeFromSeed as In,deriveHdPath as Pn,secp256k1 as Be,hash160 as Nn,encodeCashAddress as ss,lockingBytecodeToCashAddress as Rn,cashAddressToLockingBytecode as rs,binToHex as Ln,hexToBin as is,utf8ToBin as Ne,binToBase64 as Yt,base64ToBin as Vt}from"@bitauth/libauth";var kt={chipnet:{id:"chipnet",label:"chipnet",prefix:"bchtest",beacon:"bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl",beaconScripthash:"e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b",tldBeacon:"bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh",tldBeaconScripthash:"c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8",electrum:[{url:"wss://silentmode.st/electrum",ip:"195.184.247.106"},{url:"wss://coinspectrum.duckdns.org:50011",ip:"195.184.247.106"},{url:"wss://chipnet.bch.ninja:50004",ip:"135.148.236.244"},{url:"wss://chipnet.imaginary.cash:50004",ip:"66.29.152.199"},{url:"wss://tbch4.loping.net:62002",ip:"183.89.202.207"}],beaconOnly:/silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/,electrumListName:"electrum.bch",rootTldName:"tlds.bch",bootstrapTlds:["bch"],startHeight:2e5,hostingBucket:"bns",nameListUrl:"https://s3.silentmode.st:8600/bns/name-list.json",cdnIndexUrl:"https://dl.silentmode.st/bns-name-index.json",nostrTags:{names:"bns-name-list",tlds:"bns-tld-list"},explorerTx:"https://chipnet.imaginary.cash/tx/",faucets:[{name:"tbch.googol.cash",url:"https://tbch.googol.cash/"},{name:"chipnet.bch.ninja faucet",url:"https://chipnet.bch.ninja/faucet"}],operatorAddress:"bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu",accountPath:"m/44'/145'/0'",bchnRpcPort:48332},mainnet:{id:"mainnet",label:"mainnet",prefix:"bitcoincash",beacon:null,beaconScripthash:null,tldBeacon:null,tldBeaconScripthash:null,electrum:[{url:"wss://bch.imaginary.cash:50004",ip:"67.223.119.97"},{url:"wss://electrum.imaginary.cash:50004",ip:"15.204.113.162"},{url:"wss://electroncash.dk:50004",ip:"87.63.200.138"},{url:"wss://bch.loping.net:50004",ip:"183.89.202.207"},{url:"wss://bch.soul-dev.com:50004",ip:"174.141.196.204"},{url:"wss://cashnode.bch.ninja:50004",ip:"135.148.236.246"}],beaconOnly:/(?!)/,electrumListName:"electrum.bch",rootTldName:"tlds.bch",bootstrapTlds:["bch"],startHeight:null,hostingBucket:"bns-mainnet",nameListUrl:"https://s3.silentmode.st:8600/bns-mainnet/name-list.json",cdnIndexUrl:"https://dl.silentmode.st/bns-mainnet-name-index.json",nostrTags:{names:"bns-mainnet-name-list",tlds:"bns-mainnet-tld-list"},explorerTx:"https://blockchair.com/bitcoin-cash/transaction/",faucets:[],operatorAddress:null,accountPath:"m/44'/145'/0'",bchnRpcPort:8332}},Se="chipnet";function gn(){let t=typeof globalThis<"u"?globalThis.BNS_NETWORK:null,e=typeof process<"u"&&process.env?process.env.BNS_NETWORK:null;return String(t||e||Se).trim().toLowerCase()}function Pt(t){if(t&&typeof t=="object")return t;let e=t||gn(),n=kt[e];if(!n)throw new Error(`unknown network "${e}" (known: ${Object.keys(kt).join(", ")})`);return n}var T=Pt();function zt(t=T){let e=Pt(t);if(!e.beaconScripthash||!e.tldBeaconScripthash)throw new Error(`${e.label}: beacon addresses are not pinned yet (see ROADMAP-MAINNET.md \xA76)`);return e}var jo=T.beacon,Yo=T.beaconScripthash,Vo=T.tldBeacon,Xo=T.tldBeaconScripthash,Jo=T.electrum,Qo=kt.chipnet.electrum,Zo=T.electrumListName,ke="BNS1";var Te="bch",mn=/^[a-z0-9-]{1,32}$/,Ee=/^[a-z0-9-]{1,16}$/,yn=/^[a-z0-9-]{1,32}(\.[a-z0-9-]{1,16})?$/;function wn(t){return t.includes(".")?t:t+"."+Te}var xn=t=>{let e=new Uint8Array(t.length/2);for(let n=0;n<e.length;n++)e[n]=parseInt(t.substr(n*2,2),16);return e},qt=t=>[...new TextEncoder().encode(t)].map(e=>e.toString(16).padStart(2,"0")).join("");function kn(t){let n=String(t).toLowerCase().trim().replace(/^[a-z]+:\/\//,"").split("/")[0].replace(/\.$/,"").split(".").filter(Boolean);if(n.length===0)throw new Error(`invalid name "${t}"`);let o,s;if(n.length===1?(o=n[0],s=Te):(s=n[n.length-1],o=n[n.length-2]),!mn.test(o)||!Ee.test(s))throw new Error(`invalid name "${t}" (allowed: <label>.<tld>, [a-z0-9-])`);return`${
feat(market): names can be priced in US dollars — a covenant holds the certificate, a gateway-signed oracle sets the BCH amount A BCH-priced offer is a signed transaction with a fixed amount, so a seller who wants "$500" had to relist whenever the rate moved. The new USD mode moves the certificate into a small CashScript covenant (contracts/usd-listing.cash). Anyone may buy it by paying the seller target_cents / price, where the price comes from a message signed by the gateway's oracle key — the median of five exchanges the site already uses. The payout is clamped to a floor/ceiling band the seller sets, and the seller can reclaim the certificate at any time. What the chain can and cannot enforce, stated plainly in the contract and the UI: script can require a transaction to be mined no earlier than a time, never no later, so a quote cannot expire on-chain. The band is the guarantee, and quotes are bound to the listing's outpoint so cancelling and relisting voids every quote ever issued. An offline libauth VM test covers list, buy, underpayment, foreign-listing quotes, forged quotes, band clamping, cancel and a non-seller cancel; on chipnet aloevera.test was listed at $0.30, bought by a second wallet for 119,090 sat at $251.91, listed again by the new owner and cancelled. Gateway: GET /api/price/oracle[?txid=] serves the oracle public key and per-listing signed quotes; the key lives in oracle.key beside the gateway, never in the repo. POST /api/market accepts kind "usd" after reproducing the redeem script from the stated terms and finding the certificate in that covenant; such listings are pruned once the covenant output is spent; DELETE accepts the listing's seller while the index shows the covenant as holder. Dashboard: the Sell tab has "Price is fixed in: US dollars / BCH", a band control with the resulting BCH range, and cancel through the covenant. Market: USD listings show the dollar price, today's BCH equivalent and the band; buying fetches a quote and completes the covenant spend. Studio no longer sends a needless UPD when the registry view is stale, and missing files on a name now get a real 404 page.
2026-09-20 19:20:46 +02:00
`))>=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(e,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:e,params:n})+`
`),setTimeout(()=>{this.pending.has(o)&&(this.pending.delete(o),r(new Error(`electrum timeout: ${e}`)))},2e4)})}close(){try{this.ws.close()}catch{}}},bn=t=>typeof t=="string"?{url:t}:t;async function Sn(t,e,{directIP:n=!1}={}){let o;for(let s of t){let r=bn(s);try{return await new It(r.url,e).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 It(i,e,{servername:a.hostname}).connect()}catch(a){o=a}}throw o??new Error("no electrum server reachable")}function _e(t){if(!t||!t.startsWith("6a"))return null;let e=xn(t),n=1,o=[];for(;n<e.length;){let r=e[n++],a;if(r<=75)a=r;else if(r===76)a=e[n++];else if(r===77)a=e[n]|e[n+1]<<8,n+=2;else return null;o.push(e.slice(n,n+a)),n+=a}let s=new TextDecoder().decode(o[0]??new Uint8Array);if(!s.startsWith(ke))return null;try{let r=JSON.parse(s.slice(ke.length));if(Tn.has(r.o)&&typeof r.n=="string")return r}catch{}return null}var Tn=new Set(["REG","UPD","TREG","TUPD"]);function Oe(t,e){let n=t.height>0?t.height:1/0,o=e.height>0?e.height:1/0;return n-o||t.tx_hash.localeCompare(e.tx_hash)}function ve(t,e){let n=new Set(t.map(i=>i.tx_hash)),o=new Map(t.map(i=>{let l=new Set;for(let c of e.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=[...t];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 Gt(t){let e=t.lastIndexOf(".");return e<0?t:t.slice(e+1)}function En(t,e,n){let o=n.get(t);return o?o.regHeight>0&&e>0?o.regHeight<=e:!0:!1}async function jt(t,e=T){let{tldBeaconScripthash:n}=zt(e),o=typeof t.unionHistory=="function"?(await t.unionHistory(n)).history:await t.call("blockchain.scripthash.get_history",[n]);o.sort(Oe);let s=new Map;for(let{tx_hash:a}of o)try{s.set(a,await t.call("blockchain.transaction.get",[a,!0]))}catch{}let r=new Map;for(let{tx_hash:a,height:i}of ve(o.filter(l=>s.has(l.tx_hash)),s)){let c=s.get(a).vout||[],h=null;for(let d of c)h=_e(d.scriptPubKey?.hex)??h;if(!h||typeof h.n!="string"||!Ee.test(h.n))continue;let u=h.n;if(h.o==="TREG"){if(r.has(u))continue;let d=qt(u),p=c.find(m=>m.tokenData?.nft&&m.tokenData.nft.commitment===d);if(!p)continue;r.set(u,{category:p.tokenData.category,regHeight:i,regTxid:a,mintScriptHex:p.scriptPubKey?.hex??null,records:h.r??{},updates:[{height:i,txid:a,records:h.r??{}}]})}else if(h.o==="TUPD"){let d=r.get(u);if(!d||!c.some(p=>p.tokenData&&p.tokenData.category===d.category))continue;d.records=h.r??{},d.updates.push({height:i,txid:a,records:d.records})}}return r}function _n(t,e){if(!t)return{};let n=t.updates||[];if(!n.length)return t.records||{};let o={};for(let s of n)e>0&&(s.height<=0||s.height>e)||(o=s.records||{});return o}function On(t,e){let n=_n(t,e);return n.policy==="cosign"||n.hidden===1||n.hidden===!0||n.hidden==="1"}function vn(t,e,n){let o=qt(n);return(t||[]).some(s=>s.tokenData&&s.tokenData.category===e.category&&s.tokenData.nft&&s.tokenData.nft.commitment===o)}function be(t){if(!t||!t.scriptPubKey)return null;let e=t.scriptPubKey;return typeof e.address=="string"?e.address:Array.isArray(e.addresses)&&e.addresses.length?e.addresses[0]:null}async function bt({network:t=T,electrum:e=Pt(t).electrum,WebSocket:n,client:o,directIP:s=!1,enforceTld:r=!1,cosignGate:a=!0}={}){let i=zt(t),l=o??await Sn(e,n,{directIP:s});try{let c=null;if(r||a)try{c=await jt(l,i)}catch(m){if(r)throw m;c=null}let h=null,u;if(typeof l.unionHistory=="function"){let m=await l.unionHistory(i.beaconScripthash);u=m.history,h={servers:m.servers,unreachable:m.unreachable,disagreements:m.disagreements}}else u=await l.call("blockchain.scripthash.get_history",[i.beaconScripthash]);u.sort(Oe);let d=new Map;for(let{tx_hash:m}of u)try{d.set(m,await l.call("blockchain.transaction.get",[m,!0]))}catch{}let p=new Map;for(let{tx_hash:m,height:f}of ve(u.filter(g=>d.has(g.tx_hash)),d)){let y=d.get(m).vout||[],w=null;for(let S of y)w=_e(S.scriptPubKey?.hex)??w;if(!w)continue;let x=w.n;if(!yn.test(x))continue;let _=wn(x);if(w.o==="REG"){if(p.ha
feat(market): names can be priced in US dollars — a covenant holds the certificate, a gateway-signed oracle sets the BCH amount A BCH-priced offer is a signed transaction with a fixed amount, so a seller who wants "$500" had to relist whenever the rate moved. The new USD mode moves the certificate into a small CashScript covenant (contracts/usd-listing.cash). Anyone may buy it by paying the seller target_cents / price, where the price comes from a message signed by the gateway's oracle key — the median of five exchanges the site already uses. The payout is clamped to a floor/ceiling band the seller sets, and the seller can reclaim the certificate at any time. What the chain can and cannot enforce, stated plainly in the contract and the UI: script can require a transaction to be mined no earlier than a time, never no later, so a quote cannot expire on-chain. The band is the guarantee, and quotes are bound to the listing's outpoint so cancelling and relisting voids every quote ever issued. An offline libauth VM test covers list, buy, underpayment, foreign-listing quotes, forged quotes, band clamping, cancel and a non-seller cancel; on chipnet aloevera.test was listed at $0.30, bought by a second wallet for 119,090 sat at $251.91, listed again by the new owner and cancelled. Gateway: GET /api/price/oracle[?txid=] serves the oracle public key and per-listing signed quotes; the key lives in oracle.key beside the gateway, never in the repo. POST /api/market accepts kind "usd" after reproducing the redeem script from the stated terms and finding the certificate in that covenant; such listings are pruned once the covenant output is spent; DELETE accepts the listing's seller while the index shows the covenant as holder. Dashboard: the Sell tab has "Price is fixed in: US dollars / BCH", a band control with the resulting BCH range, and cancel through the covenant. Market: USD listings show the dollar price, today's BCH equivalent and the band; buying fetches a quote and completes the covenant spend. Studio no longer sends a needless UPD when the registry view is stale, and missing files on a name now get a real 404 page.
2026-09-20 19:20:46 +02:00
`))>=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(e,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:e,params:n})+`
`),setTimeout(()=>{this.pending.has(o)&&(this.pending.delete(o),r(new Error(`electrum timeout: ${e}`)))},2e4)})}close(){try{this.ws.close()}catch{}}};async function Le(t=Rt,e){let n=[];for(let o of t)try{return await new Zt(o,e).connect()}catch(s){n.push(`${o}: ${s?.message??s}`)}throw Fn(t.length,n)}function Fn(t,e){let n=new Error(`no electrum server reachable (tried ${t}). These use non-standard ports, which some networks and VPNs block:
${e.join(`
`)}`);return n.code="NO_ELECTRUM",n.failures=e,n}import{cashAddressToLockingBytecode as $n,lockingBytecodeToCashAddress as Mn,sha256 as Hn,hash160 as ps,binToHex as Kn,hexToBin as fs}from"@bitauth/libauth";function De(t){return Kn(Hn.hash(t).slice().reverse())}function L(t){let e=$n(t);if(typeof e=="string")throw new Error(`bad address "${t}": ${e}`);return e.bytecode}function Wn(t){return De(L(t))}function z(t){let e=L(t),n=String(t).includes(":")?t.split(":")[0]:T.prefix,o=Mn({bytecode:e,prefix:n,tokenSupport:!0});if(typeof o=="string")throw new Error(`cannot token-encode "${t}": ${o}`);return o.address}async function Ce(t,e){let n=L(e),o=De(n);return(await t.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:e,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 j(t,e){let n=[];for(let o of e)n.push(...await Ce(t,o));return n}async function st(t,e){return t.call("blockchain.transaction.broadcast",[e])}import{binToHex as Dt,hexToBin as X,utf8ToBin as ft,binToUtf8 as Ue,encodeTransaction as Gn,decodeTransaction as ws}from"@bitauth/libauth";var zn="BNS1",dt=T.beacon,ne=T.tldBeacon,B=600n,N=1000n,W=546n,te="bch",ee=200,qn=/^[a-z0-9-]{1,32}$/,Fe=/^[a-z0-9-]{1,16}$/;function Tt(t){let e=String(t).toLowerCase().trim().replace(/^\.+/,"").replace(/\.+$/,"");if(!Fe.test(e))throw new Error(`invalid TLD "${t}" (allowed: [a-z0-9-]{1,16})`);return e}var Lt=class extends Error{constructor(e){super(e),this.name="NoGenesisInputError",this.code="NO_GENESIS_INPUT"}},J=class extends Error{constructor(e,{needSats:n,haveSats:o}={}){super(e),this.name="InsufficientFundsError",this.code="INSUFFICIENT_FUNDS",this.needSats=n,this.haveSats=o}};function H(t){let e=String(t).toLowerCase().trim().replace(/\.$/,"").split(".").filter(Boolean),n,o;if(e.length===0)throw new Error(`invalid name "${t}"`);if(e.length===1?(n=e[0],o=te):(o=e[e.length-1],n=e[e.length-2]),!qn.test(n)||!Fe.test(o))throw new Error(`invalid name "${t}" (allowed: <label>.<tld>, [a-z0-9-])`);return o===te?n:`${n}.${o}`}function Q(t){return t.includes(".")?t:`${t}.${te}`}function Z(t,e,n){let o=JSON.stringify({o:t,n:e,r:n??{}}),s=ft(zn+o);if(s.length>ee)throw new Error(`records too large (${s.length} > ${ee} bytes)`);return s}function oe(t,e="REG"){return ee-Z(e,t,{}).length}function jn(t){return t.length<=75?Uint8Array.from([t.length,...t]):t.length<=255?Uint8Array.from([76,t.length,...t]):Uint8Array.from([77,t.length&255,t.length>>8&255,...t])}function et(t){return Uint8Array.from([106,...jn(t)])}function se(){return Uint8Array.from([65,...new Uint8Array(65),33,...new Uint8Array(33)])}var Yn=t=>!t.token;function M(t,e,{requireGenesis:n=!0}={}){let o=t.filter(Yn),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 Lt("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>=e)break;s.push(i),r+=i.satoshis}return{inputs:s,totalSats:r}}function gt(t){return{outpointTransactionHash:X(t.txid),outpointIndex:t.vout,sequenceNumber:4294967294,unlockingBytecode:se()}}function St(t){return{...gt(t),lockingBytecode:t.lockingBytecode,valueSatoshis:t.satoshis,...t.token?{token:{amount:t.token.amount??0n,category:X(t.token.category),...t.token.nft?{nft:{capability:t.token.nft.capability,commitment:X(t.token.nft.commitment||"")}}:{}}}:{}}}function ut(t){return Gn(t).length}function re({name:t,ownerAddress:e,ownerTokenAddress:n,changeAddress:o,records:s={},utxos:r,feeAddress:a=null,feeSats:i=0n,tldFeeAddress:l=null,tldFeeSats:c=0n,beaconAddress:h=dt,feeRate:u=1,addressToLockingBytecode:d,cosignU