site/js/bns-register.js

8 lines
49 KiB
JavaScript
Raw Normal View History

feat(bns): name marketplace and TLD-owner co-sign rule Two gaps the owner panel left open. First, a hidden TLD was only a UI gate: anyone could still broadcast a REG under it and every indexer admitted it. Second, there was no way to sell a name without trusting the other side. Co-sign rule (consensus, applied in lockstep by bns.js and resolver-web.js): a REG under a TLD whose records at that height say policy "cosign" or hidden 1 is indexed only if the transaction carries the TLD's own certificate. The certificate can only be spent by the owner's key and is re-issued to them in the same transaction, so it is a co-signature nobody can forge and nothing is consumed. The TLD map now keeps the TUPD timeline so policy is evaluated at the REG height. Owners register under their private TLDs with the certificate added from their own wallet; third parties under a "cosign" TLD build the full transaction, sign their inputs and queue it at /api/cosign, where the owner approves it from the dashboard (signCosignRequest refuses to sign unless the certificate returns to the same locking script). Marketplace: a listing is the seller's certificate input plus a price output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a bulletin board (/api/market, verified against the on-chain owner and pruned when the certificate moves). The buyer completes it in one transaction, so the seller is paid exactly when the name moves. Cancelling also spends the certificate once so the offer is void. Site: market.html, Sell sub-tab and Pending approvals in the portal, Market link in nav and footer, six dictionaries extended, cache tags bumped. Verified on chipnet: cosigned.sc registered by a throwaway wallet through the queue with the .sc certificate back at the owner; aloevera.test listed and delisted through the API. Ariadne's resolver-web.js copy and the mobile Bns.java port still need the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
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<Je;o++){let s=`${this.accountPath}/${t}/${o}`,r=We(this._root,s);if(typeof r=="string")throw new Error(`HD derivation failed at ${s}: ${r}`);let a=oe.derivePublicKeyCompressed(r.privateKey);if(typeof a=="string")throw new Error(`pubkey derivation failed: ${a}`);let i=ae(a),l={path:s,branch:t,index:o,privateKey:r.privateKey,publicKey:a,lockingBytecode:i,address:se(a,this.prefix,!1),tokenAddress:se(a,this.prefix,!0)};n.push(l),this._keys.set(je(i),l)}}get address(){return this.receive[0].address}get tokenAddress(){return this.receive[0].tokenAddress}get watchedAddresses(){return[...this.receive,...this.change].map(t=>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("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(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(`
feat(bns): name marketplace and TLD-owner co-sign rule Two gaps the owner panel left open. First, a hidden TLD was only a UI gate: anyone could still broadcast a REG under it and every indexer admitted it. Second, there was no way to sell a name without trusting the other side. Co-sign rule (consensus, applied in lockstep by bns.js and resolver-web.js): a REG under a TLD whose records at that height say policy "cosign" or hidden 1 is indexed only if the transaction carries the TLD's own certificate. The certificate can only be spent by the owner's key and is re-issued to them in the same transaction, so it is a co-signature nobody can forge and nothing is consumed. The TLD map now keeps the TUPD timeline so policy is evaluated at the REG height. Owners register under their private TLDs with the certificate added from their own wallet; third parties under a "cosign" TLD build the full transaction, sign their inputs and queue it at /api/cosign, where the owner approves it from the dashboard (signCosignRequest refuses to sign unless the certificate returns to the same locking script). Marketplace: a listing is the seller's certificate input plus a price output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a bulletin board (/api/market, verified against the on-chain owner and pruned when the certificate moves). The buyer completes it in one transaction, so the seller is paid exactly when the name moves. Cancelling also spends the certificate once so the offer is void. Site: market.html, Sell sub-tab and Pending approvals in the portal, Market link in nav and footer, six dictionaries extended, cache tags bumped. Verified on chipnet: cosigned.sc registered by a throwaway wallet through the queue with the .sc certificate back at the owner; aloevera.test listed and delisted through the API. Ariadne's resolver-web.js copy and the mobile Bns.java port still need the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
`)}`);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: <label>.<tld>, [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
`))>=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(;n<t.length;){let r=t[n++],a;if(r<=75)a=r;else if(r===76)a=t[n++];else if(r===77)a=t[n]|t[n+1]<<8,n+=2;else return null;o.push(t.slice(n,n+a)),n+=a}let s=new TextDecoder().decode(o[0]??new Uint8Array);if(!s.startsWith(xe))return null;try{let r=JSON.parse(s.slice(xe.length));if(mn.has(r.o)&&typeof r.n=="string")return r}catch{}return null}var mn=new Set(["REG","UPD","TREG","TUPD"]);function ve(e,t){let n=e.height>0?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)))contin