-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
48 lines (44 loc) · 11.7 KB
/
script.js
File metadata and controls
48 lines (44 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
'use strict';
class BitcoinScriptEngine {
constructor() {
this.stack = [];
this.opcodes = this.initializeOpcodes();
this.sampleScripts = this.initializeSampleScripts();
}
initializeOpcodes() {
return {
OP_DUP:{description:'Duplicate the top stack item',category:'Stack',execute:()=>this.dup()},OP_2DUP:{description:'Duplicate the top two stack items',category:'Stack',execute:()=>this.dup2()},OP_DROP:{description:'Remove the top stack item',category:'Stack',execute:()=>this.drop()},OP_SWAP:{description:'Swap the top two stack items',category:'Stack',execute:()=>this.swap()},OP_OVER:{description:'Copy the second-to-top item to the top',category:'Stack',execute:()=>this.over()},OP_ROT:{description:'Rotate the top three stack items',category:'Stack',execute:()=>this.rot()},
OP_ADD:{description:'Add the top two numeric values',category:'Arithmetic',execute:()=>this.add()},OP_SUB:{description:'Subtract top value from second value',category:'Arithmetic',execute:()=>this.sub()},OP_MUL:{description:'Educational-only multiply operation',category:'Arithmetic',execute:()=>this.mul()},OP_DIV:{description:'Educational-only integer division',category:'Arithmetic',execute:()=>this.div()},OP_MOD:{description:'Educational-only modulo',category:'Arithmetic',execute:()=>this.mod()},
OP_EQUAL:{description:'Return 1 when top two items match',category:'Comparison',execute:()=>this.equal()},OP_EQUALVERIFY:{description:'Run OP_EQUAL then OP_VERIFY',category:'Comparison',execute:()=>this.equalVerify()},OP_1EQUAL:{description:'Return 1 when top item equals 1',category:'Comparison',execute:()=>this.oneEqual()},OP_0NOTEQUAL:{description:'Return 1 when top item is not 0',category:'Comparison',execute:()=>this.zeroNotEqual()},
OP_AND:{description:'Educational-only bitwise AND',category:'Bitwise',execute:()=>this.and()},OP_OR:{description:'Educational-only bitwise OR',category:'Bitwise',execute:()=>this.or()},OP_XOR:{description:'Educational-only bitwise XOR',category:'Bitwise',execute:()=>this.xor()},OP_NOT:{description:'Return 1 for 0, otherwise 0',category:'Logical',execute:()=>this.not()},OP_BOOLAND:{description:'Boolean AND of top two items',category:'Logical',execute:()=>this.boolAnd()},OP_BOOLOR:{description:'Boolean OR of top two items',category:'Logical',execute:()=>this.boolOr()},
OP_0:{description:'Push 0',category:'Constants',execute:()=>this.push('0')},OP_1:{description:'Push 1',category:'Constants',execute:()=>this.push('1')},OP_2:{description:'Push 2',category:'Constants',execute:()=>this.push('2')},OP_3:{description:'Push 3',category:'Constants',execute:()=>this.push('3')},OP_4:{description:'Push 4',category:'Constants',execute:()=>this.push('4')},OP_5:{description:'Push 5',category:'Constants',execute:()=>this.push('5')},OP_6:{description:'Push 6',category:'Constants',execute:()=>this.push('6')},OP_7:{description:'Push 7',category:'Constants',execute:()=>this.push('7')},OP_8:{description:'Push 8',category:'Constants',execute:()=>this.push('8')},OP_9:{description:'Push 9',category:'Constants',execute:()=>this.push('9')},OP_10:{description:'Push 10',category:'Constants',execute:()=>this.push('10')},
OP_VERIFY:{description:'Fail unless top item is true',category:'Verification',execute:()=>this.verify()},OP_RETURN:{description:'Immediately fail execution',category:'Verification',execute:()=>this.returnOp()}
};
}
initializeSampleScripts(){return{basic:'OP_1 OP_2 OP_ADD OP_DUP',comparison:'OP_5 OP_3 OP_ADD OP_8 OP_EQUAL',stack:'OP_1 OP_2 OP_3 OP_ROT OP_SWAP',verify:'OP_2 OP_3 OP_ADD OP_5 OP_EQUALVERIFY OP_1'};}
tokenize(script){return script.trim().split(/\s+/).filter(Boolean);} isLiteral(t){return/^-?\d+$/.test(t);} push(v){this.stack.push(String(v));}
requireStackSize(n,m='Not enough items on stack'){if(this.stack.length<n)throw new Error(m);} popNumber(){this.requireStackSize(1,'Stack is empty');const v=this.stack.pop();const n=Number.parseInt(v,10);if(!Number.isSafeInteger(n))throw new Error(`Invalid number: ${v}`);return n;} isTrue(v){return v!=='0'&&v!=='';}
executeToken(token){if(this.opcodes[token]){this.opcodes[token].execute();return;}if(this.isLiteral(token)){this.push(token);return;}throw new Error(`Unsupported token: ${token}`);}
buildTrace(script){const tokens=this.tokenize(script);this.stack=[];const trace=[];for(let i=0;i<tokens.length;i++){const token=tokens[i];const before=[...this.stack];try{this.executeToken(token);trace.push({index:i,token,before,after:[...this.stack],ok:true,error:null});}catch(e){trace.push({index:i,token,before,after:[...this.stack],ok:false,error:e.message});break;}}return trace;}
dup(){this.requireStackSize(1,'Stack is empty');this.push(this.stack.at(-1));} dup2(){this.requireStackSize(2);this.push(this.stack.at(-2));this.push(this.stack.at(-1));} drop(){this.requireStackSize(1,'Stack is empty');this.stack.pop();} swap(){this.requireStackSize(2);const a=this.stack.pop(),b=this.stack.pop();this.push(a);this.push(b);} over(){this.requireStackSize(2);this.push(this.stack.at(-2));} rot(){this.requireStackSize(3);const a=this.stack.pop(),b=this.stack.pop(),c=this.stack.pop();this.push(b);this.push(a);this.push(c);}
add(){const b=this.popNumber(),a=this.popNumber();this.push(a+b);} sub(){const b=this.popNumber(),a=this.popNumber();this.push(a-b);} mul(){const b=this.popNumber(),a=this.popNumber();this.push(a*b);} div(){const b=this.popNumber(),a=this.popNumber();if(b===0)throw new Error('Division by zero');this.push(Math.trunc(a/b));} mod(){const b=this.popNumber(),a=this.popNumber();if(b===0)throw new Error('Modulo by zero');this.push(a%b);} equal(){this.requireStackSize(2);this.push(this.stack.pop()===this.stack.pop()?'1':'0');} equalVerify(){this.equal();this.verify();} oneEqual(){this.requireStackSize(1,'Stack is empty');this.push(this.stack.pop()==='1'?'1':'0');} zeroNotEqual(){this.requireStackSize(1,'Stack is empty');this.push(this.stack.pop()!=='0'?'1':'0');}
and(){const b=this.popNumber(),a=this.popNumber();this.push(a&b);} or(){const b=this.popNumber(),a=this.popNumber();this.push(a|b);} xor(){const b=this.popNumber(),a=this.popNumber();this.push(a^b);} not(){this.push(this.popNumber()===0?'1':'0');} boolAnd(){this.requireStackSize(2);const b=this.stack.pop(),a=this.stack.pop();this.push(this.isTrue(a)&&this.isTrue(b)?'1':'0');} boolOr(){this.requireStackSize(2);const b=this.stack.pop(),a=this.stack.pop();this.push(this.isTrue(a)||this.isTrue(b)?'1':'0');} verify(){this.requireStackSize(1,'Stack is empty');if(!this.isTrue(this.stack.pop()))throw new Error('Verification failed');} returnOp(){throw new Error('OP_RETURN failed execution');}
}
class PlaygroundUI{
constructor(engine){this.engine=engine;this.trace=[];this.activeStep=-1;this.scriptInput=document.getElementById('scriptInput');this.stackContainer=document.getElementById('stackContainer');this.traceContainer=document.getElementById('traceContainer');this.opcodesGrid=document.getElementById('opcodesGrid');this.status=document.getElementById('status');this.bindEvents();this.renderOpcodes();this.resetTrace();this.loadScriptFromUrl();}
bindEvents(){document.querySelector('[data-action="run"]').addEventListener('click',()=>this.runAll());document.querySelector('[data-action="step"]').addEventListener('click',()=>this.step());document.querySelector('[data-action="reset"]').addEventListener('click',()=>this.resetTrace());document.querySelector('[data-action="clear"]').addEventListener('click',()=>this.clearScript());document.querySelector('[data-action="share"]').addEventListener('click',()=>this.shareScript());document.querySelectorAll('[data-sample]').forEach(b=>b.addEventListener('click',()=>this.loadSample(b.dataset.sample)));document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();this.runAll();}});}
compileTrace(){const script=this.scriptInput.value.trim();if(!script){this.showStatus('Enter a script first.','error');return false;}this.trace=this.engine.buildTrace(script);this.activeStep=-1;return true;}
runAll(){if(!this.compileTrace())return;this.activeStep=this.trace.length-1;this.renderTrace();this.renderStack(this.trace.at(-1)?.after||[]);const failed=this.trace.find(t=>!t.ok);this.showStatus(failed?`Execution stopped: ${failed.error}`:`Execution complete. Steps: ${this.trace.length}.`,failed?'error':'success');}
step(){if(this.trace.length===0&&!this.compileTrace())return;if(this.activeStep<this.trace.length-1)this.activeStep+=1;const row=this.trace[this.activeStep];this.renderTrace();this.renderStack(row.after);this.showStatus(row.ok?`Step ${row.index+1}: ${row.token}`:`Step ${row.index+1} failed: ${row.error}`,row.ok?'success':'error');}
resetTrace(){this.trace=[];this.activeStep=-1;this.engine.stack=[];this.renderTrace();this.renderStack([]);this.clearStatus();}
clearScript(){this.scriptInput.value='';this.resetTrace();}
async shareScript(){const script=this.scriptInput.value.trim();if(!script){this.showStatus('No script to share.','error');return;}const url=new URL(window.location.href);url.searchParams.set('script',script);try{await navigator.clipboard.writeText(url.toString());this.showStatus('Share URL copied to clipboard.','success');}catch{window.history.replaceState(null,'',url.toString());this.showStatus('Clipboard unavailable. URL updated in address bar.','error');}}
loadSample(type){const s=this.engine.sampleScripts[type];if(!s)return;this.scriptInput.value=s;this.resetTrace();this.showStatus(`Loaded sample: ${type}.`,'success');}
loadScriptFromUrl(){const s=new URLSearchParams(window.location.search).get('script');if(s){this.scriptInput.value=s;this.runAll();}}
renderTrace(){this.traceContainer.textContent='';if(this.trace.length===0){const p=document.createElement('p');p.textContent='Trace output will appear here.';this.traceContainer.appendChild(p);return;}this.trace.forEach((t,i)=>{const row=document.createElement('div');row.className=`trace-row${i===this.activeStep?' active':''}${!t.ok?' error':''}`;const main=document.createElement('div');main.textContent=`${String(t.index+1).padStart(2,'0')} > ${t.token}`;const before=document.createElement('small');before.textContent=`before [${t.before.join(', ')}] -> after [${t.after.join(', ')}]`;row.append(main,before);if(t.error){const err=document.createElement('small');err.textContent=`error: ${t.error}`;row.appendChild(err);}this.traceContainer.appendChild(row);});}
renderStack(stack=[]){this.stackContainer.textContent='';if(stack.length===0){const p=document.createElement('p');p.textContent='[] empty stack';this.stackContainer.appendChild(p);return;}stack.forEach((item,i)=>{const row=document.createElement('div');row.className='stack-item';const v=document.createElement('span');v.textContent=item;const l=document.createElement('span');l.textContent=`#${stack.length-i}`;row.append(v,l);this.stackContainer.appendChild(row);});}
renderOpcodes(){this.opcodesGrid.textContent='';const cats=[...new Set(Object.values(this.engine.opcodes).map(o=>o.category))];cats.forEach(c=>{const h=document.createElement('h3');h.textContent=c;h.style.gridColumn='1/-1';this.opcodesGrid.appendChild(h);Object.entries(this.engine.opcodes).filter(([,o])=>o.category===c).forEach(([name,o])=>{const card=document.createElement('button');card.type='button';card.className='opcode-card';card.addEventListener('click',()=>this.insertOpcode(name));const n=document.createElement('div');n.textContent=name;const d=document.createElement('small');d.textContent=o.description;card.append(n,d);this.opcodesGrid.appendChild(card);});});}
insertOpcode(name){const current=this.scriptInput.value.trim();this.scriptInput.value=current?`${current} ${name}`:name;this.resetTrace();this.scriptInput.focus();}
showStatus(msg,type){this.status.textContent=`${type.toUpperCase()}: ${msg}`;} clearStatus(){this.status.textContent='';}
}
window.addEventListener('DOMContentLoaded',()=>new PlaygroundUI(new BitcoinScriptEngine()));