site/js/bns-register.js

8 lines
57 KiB
JavaScript
Raw Normal View History

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
import{generateBip39Mnemonic as dn,deriveSeedFromBip39Mnemonic as ye,deriveHdPrivateNodeFromSeed as hn,deriveHdPath as pn,secp256k1 as me,hash160 as fn,encodeCashAddress as Ko,lockingBytecodeToCashAddress as gn,cashAddressToLockingBytecode as Go,binToHex as yn,hexToBin as Wo,utf8ToBin as ke,binToBase64 as Ht,base64ToBin as Kt}from"@bitauth/libauth";var Et="bchtest";var Gt="m/44'/145'/0'",mn=5,At="bns.wallet.v1";function be(t){let e=fn(t);return Uint8Array.from([118,169,20,...e,136,172])}function we(t,e,n){let o=gn({bytecode:be(t),prefix:e,tokenSupport:n});if(typeof o=="string")throw new Error(`address encoding failed: ${o}`);return o.address}var Wt=class t{constructor(e,n,o=Et,s=Gt){this.mnemonic=e,this.prefix=o,this.accountPath=s,this._root=hn(n),this._keys=new Map,this._derive()}static async create(e=Et,n=Gt){let o=dn();return new t(o,await ye(o),e,n)}static async fromMnemonic(e,n=Et,o=Gt){let s=String(e).trim().toLowerCase().replace(/\s+/g," "),r=await ye(s);if(typeof r=="string")throw new Error(`invalid recovery phrase: ${r}`);return new t(s,r,n,o)}_derive(){this.receive=[],this.change=[];for(let[e,n]of[[0,this.receive],[1,this.change]])for(let o=0;o<mn;o++){let s=`${this.accountPath}/${e}/${o}`,r=pn(this._root,s);if(typeof r=="string")throw new Error(`HD derivation failed at ${s}: ${r}`);let a=me.derivePublicKeyCompressed(r.privateKey);if(typeof a=="string")throw new Error(`pubkey derivation failed: ${a}`);let i=be(a),l={path:s,branch:e,index:o,privateKey:r.privateKey,publicKey:a,lockingBytecode:i,address:we(a,this.prefix,!1),tokenAddress:we(a,this.prefix,!0)};n.push(l),this._keys.set(yn(i),l)}}get address(){return this.receive[0].address}get tokenAddress(){return this.receive[0].tokenAddress}get watchedAddresses(){return[...this.receive,...this.change].map(e=>e.address)}keyFor(e){let n=this._keys.get(e);return n?{privateKey:n.privateKey,publicKey:n.publicKey}:null}signMessage(e,n){if(!(e instanceof Uint8Array)||e.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=me.signMessageHashRecoverableCompact(s.privateKey,e);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(e){let n=crypto.getRandomValues(new Uint8Array(16)),o=crypto.getRandomValues(new Uint8Array(12)),s=await xe(e,n),r=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:o},s,ke(this.mnemonic)));return JSON.stringify({v:1,kdf:"PBKDF2-SHA256",iterations:qt,prefix:this.prefix,salt:Ht(n),iv:Ht(o),ct:Ht(r)})}static async decrypt(e,n){let o=typeof e=="string"?JSON.parse(e):e,s=await xe(n,Kt(o.salt),o.iterations??qt),r;try{r=await crypto.subtle.decrypt({name:"AES-GCM",iv:Kt(o.iv)},s,Kt(o.ct))}catch{throw new Error("wrong password")}return t.fromMnemonic(new TextDecoder().decode(r),o.prefix??Et)}async save(e,n=globalThis.localStorage){n.setItem(At,await this.encrypt(e))}static async load(e,n=globalThis.localStorage){let o=n.getItem(At);return o?t.decrypt(o,e):null}static exists(e=globalThis.localStorage){return!!e?.getItem(At)}static forget(e=globalThis.localStorage){e.removeItem(At)}},qt=25e4;async function xe(t,e,n=qt){let o=await crypto.subtle.importKey("raw",ke(String(t)),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:e,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"],zt=class{constructor(e,n){this.url=e,this.WS=n||globalThis.WebSocket,this.id=0,this.pending=new Map,this.buf=""}connect(){return new Promise((e,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"]),e(this)}catch(i){n(i)}},r=i=>n(new Error("el
`))>=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 Se(t=Bt,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 wn(t.length,n)}function wn(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 xn,lockingBytecodeToCashAddress as kn,sha256 as bn,hash160 as Yo,binToHex as Sn,hexToBin as Vo}from"@bitauth/libauth";function Te(t){return Sn(bn.hash(t).slice().reverse())}function L(t){let e=xn(t);if(typeof e=="string")throw new Error(`bad address "${t}": ${e}`);return e.bytecode}function Tn(t){return Te(L(t))}function W(t){let e=L(t),n=String(t).includes(":")?t.split(":")[0]:"bchtest",o=kn({bytecode:e,prefix:n,tokenSupport:!0});if(typeof o=="string")throw new Error(`cannot token-encode "${t}": ${o}`);return o.address}async function _e(t,e){let n=L(e),o=Te(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 z(t,e){let n=[];for(let o of e)n.push(...await _e(t,o));return n}async function ot(t,e){return t.call("blockchain.transaction.broadcast",[e])}import{binToHex as Pt,hexToBin as V,utf8ToBin as pt,binToUtf8 as Oe,encodeTransaction as _n,decodeTransaction as Qo}from"@bitauth/libauth";var On="BNS1",ut="bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl",Vt="bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh",v=600n,I=1000n,K=546n,jt="bch",Yt=200,En=/^[a-z0-9-]{1,32}$/,Ee=/^[a-z0-9-]{1,16}$/;function kt(t){let e=String(t).toLowerCase().trim().replace(/^\.+/,"").replace(/\.+$/,"");if(!Ee.test(e))throw new Error(`invalid TLD "${t}" (allowed: [a-z0-9-]{1,16})`);return e}var It=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 M(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=jt):(o=e[e.length-1],n=e[e.length-2]),!En.test(n)||!Ee.test(o))throw new Error(`invalid name "${t}" (allowed: <label>.<tld>, [a-z0-9-])`);return o===jt?n:`${n}.${o}`}function X(t){return t.includes(".")?t:`${t}.${jt}`}function Q(t,e,n){let o=JSON.stringify({o:t,n:e,r:n??{}}),s=pt(On+o);if(s.length>Yt)throw new Error(`records too large (${s.length} > ${Yt} bytes)`);return s}function Jt(t,e="REG"){return Yt-Q(e,t,{}).length}function vn(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 tt(t){return Uint8Array.from([106,...vn(t)])}function Xt(){return Uint8Array.from([65,...new Uint8Array(65),33,...new Uint8Array(33)])}var An=t=>!t.token;function $(t,e,{requireGenesis:n=!0}={}){let o=t.filter(An),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 It("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 ft(t){return{outpointTransactionHash:V(t.txid),outpointIndex:t.vout,sequenceNumber:4294967294,unlockingBytecode:Xt()}}function xt(t){return{...ft(t),lockingBytecode:t.lockingBytecode,valueSatoshis:t.satoshis,...t.token?{token:{amount:t.token.amount??0n,category:V(t.token.category),...t.token.nft?{nft:{capability:t.token.nft.capability,commitment:V(t.token.nft.commitment||"")}}:{}}}:{}}}function lt(t){return _n(t).length}function Qt({name:t,ownerAddress:e,ownerTokenAddress:n,changeAddress:o,records:s={},utxos:r,feeAddress:a=null,feeSats:i=0n,tldFeeAddress:l=
`))>=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{}}},Fn=t=>typeof t=="string"?{url:t}:t;async function $n(t,e,{directIP:n=!1}={}){let o;for(let s of t){let r=Fn(s);try{return await new Rt(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 Rt(i,e,{servername:a.hostname}).connect()}catch(a){o=a}}throw o??new Error("no electrum server reachable")}function De(t){if(!t||!t.startsWith("6a"))return null;let e=Dn(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(Le))return null;try{let r=JSON.parse(s.slice(Le.length));if(Mn.has(r.o)&&typeof r.n=="string")return r}catch{}return null}var Mn=new Set(["REG","UPD","TREG","TUPD"]);function Ue(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 Fe(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 ne(t){let e=t.lastIndexOf(".");return e<0?t:t.slice(e+1)}function Hn(t,e,n){let o=n.get(t);return o?o.regHeight>0&&e>0?o.regHeight<=e:!0:!1}async function se(t){let e=typeof t.unionHistory=="function"?(await t.unionHistory(Ne)).history:await t.call("blockchain.scripthash.get_history",[Ne]);e.sort(Ue);let n=new Map;for(let{tx_hash:s}of e)try{n.set(s,await t.call("blockchain.transaction.get",[s,!0]))}catch{}let o=new Map;for(let{tx_hash:s,height:r}of Fe(e.filter(a=>n.has(a.tx_hash)),n)){let i=n.get(s).vout||[],l=null;for(let h of i)l=De(h.scriptPubKey?.hex)??l;if(!l||typeof l.n!="string"||!Ce.test(l.n))continue;let c=l.n;if(l.o==="TREG"){if(o.has(c))continue;let h=oe(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 Kn(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 Gn(t,e){let n=Kn(t,e);return n.policy==="cosign"||n.hidden===1||n.hidden===!0||n.hidden==="1"}function Wn(t,e,n){let o=oe(n);return(t||[]).some(s=>s.tokenData&&s.tokenData.category===e.category&&s.tokenData.nft&&s.tokenData.nft.commitment===o)}function Re(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 St({electrum:t=Cn,WebSocket:e,client:n,directIP:o=!1,enforceTld:s=!1,cosignGate:r=!0}={}){let a=n??await $n(t,e,{directIP:o});try{let i=null;if(s||r)try{i=await se(a)}catch(d){if(s)throw d;i=null}let l=null,c;if(typeof a.unionHistory=="function"){let d=await a.unionHistory(Pe);c=d.history,l={servers:d.servers,unreachable:d.unreachable,disagreements:d.disagreements}}else c=await a.call("blockchain.scripthash.get_history",[Pe]);c.sort(Ue);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:p}of Fe(c.filter(m=>h.has(m.tx_hash)),h)){let g=h.get(d).vout||[],f=null;for(let x of g)f=De(x.scriptPubKey?.hex)??f;if(!f)continue;let y=f.n;if(!Ln.test(y))continue;let w=Rn(y);if(f.o==="REG"){if(u.has(w)||s&&!Hn(ne(w),p,i))continue;if(r&&i){let b=i.get(ne(w));if(b&&Gn(b,p)&&!Wn(g,b,ne(w)))contin