diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index b719df76b..92f576856 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -155,7 +155,14 @@ ScriptSig (ASM) - + + + ScriptSig (HEX) diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index b31a14535..953167945 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -15,7 +15,7 @@ import { OrdApiService } from '@app/services/ord-api.service'; import { Inscription } from '@app/shared/ord/inscription.utils'; import { Etching, Runestone } from '@app/shared/ord/rune.utils'; import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils'; -import { processInputSignatures, Sighash, SigInfo } from '../../shared/transaction.utils'; +import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/shared/transaction.utils'; @Component({ selector: 'app-transactions-list', @@ -62,15 +62,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { selectedSig: { txIndex: number, vindex: number, sig: SigInfo } | null = null; sigHighlights: { vin: boolean[], vout: boolean[] } = { vin: [], vout: [] }; - sighashLabels: { [sighash: string]: string } = { - '0': 'SIGHASH_DEFAULT', - '1': 'SIGHASH_ALL', - '2': 'SIGHASH_NONE', - '3': 'SIGHASH_SINGLE', - '129': 'SIGHASH_ALL | ACP', - '130': 'SIGHASH_NONE | ACP', - '131': 'SIGHASH_SINGLE | ACP', - }; + sighashLabels = SighashLabels; constructor( public stateService: StateService, diff --git a/frontend/src/app/shared/components/asm/asm.component.html b/frontend/src/app/shared/components/asm/asm.component.html new file mode 100644 index 000000000..a1f2801e9 --- /dev/null +++ b/frontend/src/app/shared/components/asm/asm.component.html @@ -0,0 +1,30 @@ +
+ @for (instruction of instructions; track instruction.instruction) { + OP_{{instruction.instruction}} + @for (arg of instruction.args; track arg) { + + + + + + {{arg.slice(0, -2)}}{{arg.slice(-2)}} + + + {{ arg }} + + + + {{ arg }} + + } +
+ } +
diff --git a/frontend/src/app/shared/components/asm/asm.component.scss b/frontend/src/app/shared/components/asm/asm.component.scss new file mode 100644 index 000000000..ce9b2b22a --- /dev/null +++ b/frontend/src/app/shared/components/asm/asm.component.scss @@ -0,0 +1,53 @@ +.sig { + cursor: pointer; + + + &.sighash-0 { + --sigColor: var(--green); + } + &.sighash-1 { + --sigColor: var(--green); + } + &.sighash-2 { + --sigColor: gold; + } + &.sighash-3 { + --sigColor: cornflowerblue; + } + &.sighash-129 { + --sigColor: darkviolet; + } + &.sighash-130 { + --sigColor: darkorange; + } + &.sighash-131 { + --sigColor: var(--pink); + } + &.sig-no-lock { + --sigColor: var(--red); + } + color: var(--sigColor); + + &:hover, &.hovered { + color: color-mix(in srgb, var(--sigColor) 60%, white); + } + + ::ng-deep svg path { + pointer-events: auto; + } +} + +.sig-key { + position: absolute; + top: 0; + left: 0; + transition: transform 0.2s ease; + pointer-events: none; + + &.sig-inline { + position: relative; + pointer-events: auto; + margin-left: 4px; + margin-right: -4px; + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/asm/asm.component.ts b/frontend/src/app/shared/components/asm/asm.component.ts new file mode 100644 index 000000000..66ae5df4b --- /dev/null +++ b/frontend/src/app/shared/components/asm/asm.component.ts @@ -0,0 +1,178 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core'; +import { SigInfo, SighashLabels } from '@app/shared/transaction.utils'; + +@Component({ + selector: 'app-asm', + templateUrl: './asm.component.html', + styleUrls: ['./asm.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AsmComponent { + @Input() asm: string; + @Input() crop: number = 0; + @Input() annotations: { + signatures: Record, + selectedSig: SigInfo | null + } = { + signatures: {}, + selectedSig: null + }; + @Output() showSigInfo = new EventEmitter(); + @Output() hideSigInfo = new EventEmitter(); + + instructions: { instruction: string, args: string[] }[] = []; + sighashLabels: Record = SighashLabels; + + ngOnInit(): void { + this.parseASM(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['asm']) { + this.parseASM(); + } + } + + parseASM(): void { + let instructions = this.asm.split('OP_'); + // trim instructions to a whole number of instructions with at most `crop` characters total + if (this.crop) { + let chars = 0; + for (let i = 0; i < instructions.length; i++) { + chars += instructions[i].length + 3; + if (chars > this.crop) { + instructions = instructions.slice(0, i); + break; + } + } + } + this.instructions = instructions.filter(instruction => instruction.trim() !== '').map(instruction => { + const parts = instruction.split(' '); + return { + instruction: parts[0], + args: parts.slice(1) + }; + }); + } + + doShowSigInfo(sig: SigInfo): void { + this.showSigInfo.emit(sig); + } + + doHideSigInfo(): void { + this.hideSigInfo.emit(); + } + + readonly opcodeStyles: Map = new Map([ + // Constants + ['0', 'constants'], + ['FALSE', 'constants'], + ['TRUE', 'constants'], + ...Array.from({length: 75}, (_, i) => [`PUSHBYTES_${i + 1}`, 'constants']), + ['PUSHDATA1', 'constants'], + ['PUSHDATA2', 'constants'], + ['PUSHDATA4', 'constants'], + ['PUSHNUM_NEG1', 'constants'], + ...Array.from({length: 16}, (_, i) => [`PUSHNUM_${i + 1}`, 'constants']), + + // Control flow + ['NOP', 'control'], + ['IF', 'control'], + ['NOTIF', 'control'], + ['ELSE', 'control'], + ['ENDIF', 'control'], + ['VERIFY', 'control'], + ...Array.from({length: 70}, (_, i) => [`RETURN_${i + 186}`, 'control']), + + // Stack + ['TOALTSTACK', 'stack'], + ['FROMALTSTACK', 'stack'], + ['IFDUP', 'stack'], + ['DEPTH', 'stack'], + ['DROP', 'stack'], + ['DUP', 'stack'], + ['NIP', 'stack'], + ['OVER', 'stack'], + ['PICK', 'stack'], + ['ROLL', 'stack'], + ['ROT', 'stack'], + ['SWAP', 'stack'], + ['TUCK', 'stack'], + ['2DROP', 'stack'], + ['2DUP', 'stack'], + ['3DUP', 'stack'], + ['2OVER', 'stack'], + ['2ROT', 'stack'], + ['2SWAP', 'stack'], + + // String + ['CAT', 'splice'], + ['SUBSTR', 'splice'], + ['LEFT', 'splice'], + ['RIGHT', 'splice'], + ['SIZE', 'splice'], + + // Logic + ['INVERT', 'logic'], + ['AND', 'logic'], + ['OR', 'logic'], + ['XOR', 'logic'], + ['EQUAL', 'logic'], + ['EQUALVERIFY', 'logic'], + + // Arithmetic + ['1ADD', 'arithmetic'], + ['1SUB', 'arithmetic'], + ['2MUL', 'arithmetic'], + ['2DIV', 'arithmetic'], + ['NEGATE', 'arithmetic'], + ['ABS', 'arithmetic'], + ['NOT', 'arithmetic'], + ['0NOTEQUAL', 'arithmetic'], + ['ADD', 'arithmetic'], + ['SUB', 'arithmetic'], + ['MUL', 'arithmetic'], + ['DIV', 'arithmetic'], + ['MOD', 'arithmetic'], + ['LSHIFT', 'arithmetic'], + ['RSHIFT', 'arithmetic'], + ['BOOLAND', 'arithmetic'], + ['BOOLOR', 'arithmetic'], + ['NUMEQUAL', 'arithmetic'], + ['NUMEQUALVERIFY', 'arithmetic'], + ['NUMNOTEQUAL', 'arithmetic'], + ['LESSTHAN', 'arithmetic'], + ['GREATERTHAN', 'arithmetic'], + ['LESSTHANOREQUAL', 'arithmetic'], + ['GREATERTHANOREQUAL', 'arithmetic'], + ['MIN', 'arithmetic'], + ['MAX', 'arithmetic'], + ['WITHIN', 'arithmetic'], + + // Crypto + ['RIPEMD160', 'crypto'], + ['SHA1', 'crypto'], + ['SHA256', 'crypto'], + ['HASH160', 'crypto'], + ['HASH256', 'crypto'], + ['CODESEPARATOR', 'crypto'], + ['CHECKSIG', 'crypto'], + ['CHECKSIGVERIFY', 'crypto'], + ['CHECKMULTISIG', 'crypto'], + ['CHECKMULTISIGVERIFY', 'crypto'], + ['CHECKSIGADD', 'crypto'], + + // Locktime + ['CLTV', 'locktime'], + ['CSV', 'locktime'], + + // Reserved + ['RESERVED', 'reserved'], + ['VER', 'reserved'], + ['VERIF', 'reserved'], + ['VERNOTIF', 'reserved'], + ['RESERVED1', 'reserved'], + ['RESERVED2', 'reserved'], + ...Array.from({length: 10}, (_, i) => [`NOP${i + 1}`, 'reserved']) + ] as [string, string][]); +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 7fc7c1df9..7b54b80fa 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -18,6 +18,7 @@ import { Hex2asciiPipe } from '@app/shared/pipes/hex2ascii/hex2ascii.pipe'; import { Decimal2HexPipe } from '@app/shared/pipes/decimal2hex/decimal2hex.pipe'; import { FeeRoundingPipe } from '@app/shared/pipes/fee-rounding/fee-rounding.pipe'; import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe'; +import { AsmComponent } from '@app/shared/components/asm/asm.component'; import { AbsolutePipe } from '@app/shared/pipes/absolute/absolute.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; import { ScriptpubkeyTypePipe } from '@app/shared/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe'; @@ -147,6 +148,7 @@ import { GithubLogin } from '../components/github-login.component/github-login.c NoSanitizePipe, Hex2asciiPipe, AsmStylerPipe, + AsmComponent, AbsolutePipe, BytesPipe, VbytesPipe, diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index d51231125..ecf392ff4 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -104,6 +104,16 @@ export type SighashValue = (SighashFlag.SINGLE & SighashFlag.ANYONECANPAY) | (SighashFlag.ALL & SighashFlag.NONE); +export const SighashLabels: Record = { + '0': 'SIGHASH_DEFAULT', + '1': 'SIGHASH_ALL', + '2': 'SIGHASH_NONE', + '3': 'SIGHASH_SINGLE', + '129': 'SIGHASH_ALL | ACP', + '130': 'SIGHASH_NONE | ACP', + '131': 'SIGHASH_SINGLE | ACP', +}; + export interface SigInfo { signature: string; sighash: SighashValue; @@ -172,7 +182,7 @@ export function extractDERSignaturesASM(script_asm: string): SigInfo[] { if (isDERSig(hexData)) { const sighash = decodeSighashFlag(parseInt(hexData.slice(-2), 16)); signatures.push({ - signature: hexData.slice(0, -2), // Remove sighash byte + signature: hexData, sighash }); }