8 lines
49 KiB
JavaScript
8 lines
49 KiB
JavaScript
|
|
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(`
|
||
|
|
`)}`);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
|