├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── go.mod └── main.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 deltartificial 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧞‍♀️ Ordinals Inscriptions Decoder 2 | 3 | Decode Ordinals Inscriptions from Bitcoin raw transactions data. 4 | UTF-8 BRC-20 Fast decoder. 5 | 6 | ### Config 7 | - inputData (string) : Bitcoin inscription raw data. 8 | 9 | ### Install 10 | Requirements : Go. 11 | - `go get` 12 | - `go run .` 13 | 14 | ### Twitter 15 | @deltartificial -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module inscription-decoder 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | pointer int 16 | inputData string = "2024531bd7c0de19af5b3009033c130e757702f7d4b933aa848be3d825e090a041ac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d38003a7b2270223a226272632d3230222c226f70223a226d696e74222c227469636b223a2273617473222c22616d74223a22313030303030303030227d68" 17 | ) 18 | 19 | func readBytes(raw []byte, n int) []byte { 20 | value := raw[pointer : pointer+n] 21 | pointer += n 22 | return value 23 | } 24 | 25 | func getInitialPosition(raw []byte) (int, error) { 26 | inscriptionMark := []byte{0x00, 0x63, 0x03, 0x6f, 0x72, 0x64} 27 | pos := strings.Index(string(raw), string(inscriptionMark)) 28 | if pos == -1 { 29 | return 0, errors.New("No ordinal inscription found in transaction") 30 | } 31 | return pos + len(inscriptionMark), nil 32 | } 33 | 34 | func readContentType(raw []byte) (string, error) { 35 | OP_1 := byte(0x51) 36 | 37 | b := readBytes(raw, 1)[0] 38 | if b != OP_1 { 39 | if b != 0x01 || readBytes(raw, 1)[0] != 0x01 { 40 | return "", errors.New("Invalid byte sequence") 41 | } 42 | } 43 | 44 | size := int(readBytes(raw, 1)[0]) 45 | contentType := readBytes(raw, size) 46 | return string(contentType), nil 47 | } 48 | 49 | func readPushdata(raw []byte, opcode byte) ([]byte, error) { 50 | intOpcode := int(opcode) 51 | 52 | if 0x01 <= intOpcode && intOpcode <= 0x4b { 53 | return readBytes(raw, intOpcode), nil 54 | } 55 | 56 | numBytes := 0 57 | switch intOpcode { 58 | case 0x4c: 59 | numBytes = 1 60 | case 0x4d: 61 | numBytes = 2 62 | case 0x4e: 63 | numBytes = 4 64 | default: 65 | return nil, fmt.Errorf("Invalid push opcode %x at position %d", intOpcode, pointer) 66 | } 67 | 68 | if pointer+numBytes > len(raw) { 69 | return nil, fmt.Errorf("Invalid data length at position %d", pointer) 70 | } 71 | 72 | sizeBytes := readBytes(raw, numBytes) 73 | var size int 74 | switch numBytes { 75 | case 1: 76 | size = int(sizeBytes[0]) 77 | case 2: 78 | size = int(binary.LittleEndian.Uint16(sizeBytes)) 79 | case 4: 80 | size = int(binary.LittleEndian.Uint32(sizeBytes)) 81 | } 82 | 83 | if pointer+size > len(raw) { 84 | return nil, fmt.Errorf("Invalid data length at position %d", pointer) 85 | } 86 | 87 | return readBytes(raw, size), nil 88 | } 89 | 90 | func writeDataUri(data []byte, contentType string) { 91 | dataBase64 := base64.StdEncoding.EncodeToString(data) 92 | fmt.Printf("data:%s;base64,%s", contentType, dataBase64) 93 | } 94 | 95 | func writeFile(data []byte, filename string) { 96 | if filename == "" { 97 | filename = "out.txt" 98 | } 99 | 100 | filename = "out/" + filename 101 | 102 | i := 1 103 | baseFilename := filename 104 | for _, err := os.Stat(filename); !os.IsNotExist(err); _, err = os.Stat(filename) { 105 | i++ 106 | filename = fmt.Sprintf("%s%d", baseFilename, i) 107 | } 108 | 109 | fmt.Printf("Writing contents to file \"%s\"\n", filename) 110 | err := os.WriteFile(filename, data, 0644) 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | } 115 | 116 | func main() { 117 | raw, err := hex.DecodeString(inputData) 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | 122 | pointer, _ = getInitialPosition(raw) 123 | 124 | contentType, _ := readContentType(raw) 125 | fmt.Printf("Content type: %s\n", contentType) 126 | if readBytes(raw, 1)[0] != byte(0x00) { 127 | fmt.Println("Error: Invalid byte sequence") 128 | os.Exit(1) 129 | } 130 | 131 | data := []byte{} 132 | 133 | OP_ENDIF := byte(0x68) 134 | opcode := readBytes(raw, 1)[0] 135 | for opcode != OP_ENDIF { 136 | chunk, _ := readPushdata(raw, opcode) 137 | data = append(data, chunk...) 138 | opcode = readBytes(raw, 1)[0] 139 | } 140 | 141 | fmt.Printf("Total size: %d bytes\n", len(data)) 142 | writeFile(data, "output") 143 | fmt.Println("\nDone") 144 | } 145 | --------------------------------------------------------------------------------