├── index.ts ├── bun.lockb ├── src ├── index.ts ├── Symbol.ts ├── Flag.ts ├── types.ts ├── RuneId.ts ├── Rune.ts ├── SpacedRune.ts ├── Tag.ts ├── example.ts └── Runestone.ts ├── tsconfig.json ├── package.json ├── .gitignore └── README.md /index.ts: -------------------------------------------------------------------------------- 1 | console.log("Hello via Bun!"); -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joundy/runestone-js/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Flag"; 2 | export * from "./Rune"; 3 | export * from "./RuneId"; 4 | export * from "./Runestone"; 5 | export * from "./SpacedRune"; 6 | export * from "./Symbol"; 7 | export * from "./Tag"; 8 | export * from "./types"; 9 | -------------------------------------------------------------------------------- /src/Symbol.ts: -------------------------------------------------------------------------------- 1 | import { U8 } from "big-varuint-js"; 2 | 3 | export class Symbol { 4 | readonly symbol: U8; 5 | 6 | constructor(symbol: U8) { 7 | this.symbol = symbol; 8 | } 9 | 10 | static fromString(symbolStr: string) { 11 | if (symbolStr.length !== 1) { 12 | throw new Error("Symbol must be 1 character"); 13 | } 14 | 15 | return new Symbol(new U8(BigInt(Buffer.from(symbolStr, "utf8")[0]))); 16 | } 17 | 18 | toString() { 19 | return Buffer.from([Number(this.symbol.toValue())]).toString("utf8"); 20 | } 21 | 22 | toJSON() { 23 | return this.toString(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Flag.ts: -------------------------------------------------------------------------------- 1 | import { U8 } from "big-varuint-js"; 2 | 3 | export enum FlagEnum { 4 | Etching = 0, 5 | Terms = 1, 6 | Cenotaph = 127, 7 | } 8 | 9 | export class Flag { 10 | private flag: U8; 11 | 12 | constructor(value: U8) { 13 | this.flag = value; 14 | } 15 | 16 | set(flag: FlagEnum) { 17 | const mask = 1n << BigInt(flag); 18 | this.flag = new U8(this.flag.toValue() | mask); 19 | } 20 | 21 | hasFlag(flag: FlagEnum) { 22 | const mask = 1n << BigInt(flag); 23 | return (this.flag.toValue() & mask) !== 0n; 24 | } 25 | 26 | toValue() { 27 | return this.flag; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "react-jsx", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { U128, U32, U64, U8 } from "big-varuint-js"; 2 | import { RuneId } from "./RuneId"; 3 | import { Rune } from "./Rune"; 4 | import { Symbol } from "./Symbol"; 5 | 6 | export type Edict = { 7 | id: RuneId; 8 | amount: U128; 9 | output: U32; 10 | }; 11 | 12 | export type Terms = { 13 | amount?: U128; 14 | cap?: U128; 15 | height?: { 16 | start?: U64; 17 | end?: U64; 18 | }; 19 | offset?: { 20 | start?: U64; 21 | end?: U64; 22 | }; 23 | }; 24 | 25 | export type Etching = { 26 | divisibility?: U8; 27 | premine?: U128; 28 | rune?: Rune; 29 | spacers?: U32; 30 | symbol?: Symbol; 31 | terms?: Terms; 32 | }; 33 | 34 | export type RunestoneParams = { 35 | edicts: Edict[]; 36 | etching?: Etching; 37 | mint?: RuneId; 38 | pointer?: U32; 39 | }; 40 | -------------------------------------------------------------------------------- /src/RuneId.ts: -------------------------------------------------------------------------------- 1 | import { U32, U64 } from "big-varuint-js"; 2 | 3 | export class RuneId { 4 | readonly block; 5 | readonly tx; 6 | 7 | constructor(block: U64, tx: U32) { 8 | this.block = block; 9 | this.tx = tx; 10 | } 11 | 12 | delta(next: RuneId) { 13 | const block = next.block.toValue() - this.block.toValue(); 14 | let tx = next.tx.toValue(); 15 | if (block === 0n) { 16 | tx -= this.tx.toValue(); 17 | } 18 | 19 | return new RuneId(new U64(block), new U32(tx)); 20 | } 21 | 22 | next(next: RuneId) { 23 | const block = this.block.toValue() + next.block.toValue(); 24 | const tx = 25 | next.block.toValue() === 0n 26 | ? this.tx.toValue() + next.tx.toValue() 27 | : next.tx.toValue(); 28 | 29 | return new RuneId(new U64(block), new U32(tx)); 30 | } 31 | 32 | toJSON() { 33 | return { 34 | block: this.block.toString(), 35 | tx: this.tx.toString(), 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runestone-js", 3 | "repository": "joundy/runestone-js", 4 | "author": { 5 | "name": "Joundy", 6 | "email": "contact@joundy.me", 7 | "url": "https://github.com/joundy" 8 | }, 9 | "version": "0.3.0", 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "description": "Javascript ordinals runestone implementation", 13 | "module": "index.ts", 14 | "type": "module", 15 | "scripts": { 16 | "build": "bun run build.mjs", 17 | "prepublishOnly": "bun run build" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "keywords": [ 23 | "bun", 24 | "bitcoin", 25 | "runestone", 26 | "ordinal", 27 | "inscription", 28 | "ord" 29 | ], 30 | "devDependencies": { 31 | "bun-plugin-dts": "^0.2.1", 32 | "bun-types": "latest" 33 | }, 34 | "dependencies": { 35 | "big-varuint-js": "^0.2.1" 36 | }, 37 | "peerDependencies": { 38 | "typescript": "^5.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Rune.ts: -------------------------------------------------------------------------------- 1 | import { U128 } from "big-varuint-js"; 2 | 3 | export class Rune { 4 | readonly rune: U128; 5 | 6 | constructor(rune: U128) { 7 | this.rune = rune; 8 | } 9 | 10 | static fromString(str: string) { 11 | let number = 0n; 12 | 13 | for (let i = 0; i < str.length; i += 1) { 14 | const c = str.charAt(i); 15 | if (i > 0) { 16 | number += 1n; 17 | } 18 | number *= 26n; 19 | if (c >= "A" && c <= "Z") { 20 | number += BigInt(c.charCodeAt(0) - "A".charCodeAt(0)); 21 | } else { 22 | throw new Error(`Invalid character in rune name: ${c}`); 23 | } 24 | } 25 | 26 | return new Rune(new U128(number)); 27 | } 28 | 29 | commitBuffer() { 30 | let number = this.rune.toValue(); 31 | const arr = []; 32 | while (number >> 8n > 0) { 33 | arr.push(Number(number & 0b11111111n)); 34 | number >>= 8n; 35 | } 36 | arr.push(Number(number)); 37 | 38 | return Buffer.from(arr); 39 | } 40 | 41 | toString() { 42 | let n = this.rune.toValue(); 43 | 44 | n += 1n; 45 | let str = ""; 46 | while (n > 0n) { 47 | str += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Number((n - 1n) % 26n)]; 48 | n = (n - 1n) / 26n; 49 | } 50 | 51 | return str.split("").reverse().join(""); 52 | } 53 | 54 | toJSON() { 55 | return this.toString(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/SpacedRune.ts: -------------------------------------------------------------------------------- 1 | import { U32 } from "big-varuint-js"; 2 | import { Rune } from "./Rune"; 3 | 4 | export class SpacedRune { 5 | readonly rune: Rune; 6 | readonly spacers: U32; 7 | 8 | constructor(rune: Rune, spacers: U32) { 9 | this.rune = rune; 10 | this.spacers = spacers; 11 | } 12 | 13 | static fromString(str: string) { 14 | let runeStr = ""; 15 | let spacers = 0; 16 | 17 | for (let i = 0; i < str.length; i++) { 18 | const char = str[i]; 19 | 20 | // valid character 21 | if (/[A-Z]/.test(char)) { 22 | runeStr += char; 23 | } else if (char === "." || char === "•") { 24 | const flag = 1 << (runeStr.length - 1); 25 | if ((spacers & flag) !== 0) { 26 | throw new Error("Double spacer"); 27 | } 28 | 29 | spacers |= flag; 30 | } else { 31 | throw new Error("Invalid spacer character"); 32 | } 33 | } 34 | 35 | if (32 - Math.clz32(spacers) >= runeStr.length) { 36 | throw new Error("Trailing spacer"); 37 | } 38 | 39 | return new SpacedRune(Rune.fromString(runeStr), new U32(BigInt(spacers))); 40 | } 41 | 42 | toString() { 43 | const runeStr = this.rune.toString(); 44 | 45 | let result = ""; 46 | for (let i = 0; i < runeStr.length; i += 1) { 47 | const str = runeStr[i]; 48 | result += str; 49 | 50 | if (this.spacers.toValue() & (1n << BigInt(i))) { 51 | result += "•"; 52 | } 53 | } 54 | 55 | return result; 56 | } 57 | 58 | toJSON() { 59 | return this.toString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | -------------------------------------------------------------------------------- /src/Tag.ts: -------------------------------------------------------------------------------- 1 | import { 2 | U128, 3 | U16, 4 | U32, 5 | U64, 6 | U8, 7 | Varuint, 8 | decodeBigVaruint, 9 | } from "big-varuint-js"; 10 | export enum Tag { 11 | Body = 0, 12 | Flags = 2, 13 | Rune = 4, 14 | Premine = 6, 15 | Cap = 8, 16 | Amount = 10, 17 | HeightStart = 12, 18 | HeightEnd = 14, 19 | OffsetStart = 16, 20 | OffsetEnd = 18, 21 | Mint = 20, 22 | Pointer = 22, 23 | Cenotaph = 126, 24 | 25 | Divisibility = 1, 26 | Spacers = 3, 27 | Symbol = 5, 28 | Nop = 127, 29 | } 30 | 31 | export enum ValueType { 32 | U8, 33 | U16, 34 | U32, 35 | U64, 36 | U128, 37 | } 38 | 39 | export class TagPayload { 40 | payloads: number[] = []; 41 | edicts: bigint[] = []; 42 | 43 | tagMap = new Map(); 44 | 45 | constructor(buff?: Buffer) { 46 | if (!buff) { 47 | return; 48 | } 49 | this.payloads.push(...buff); 50 | } 51 | 52 | decode() { 53 | const arr: bigint[] = []; 54 | let startI = -1; 55 | for (let i = 0; i < this.payloads.length; i += 1) { 56 | const byte = this.payloads[i]; 57 | // maximum varuint per byte value is 127 58 | if ((byte & 0b1000_0000) === 0) { 59 | if (startI !== -1) { 60 | arr.push( 61 | decodeBigVaruint(Buffer.from(this.payloads.slice(startI, i + 1))), 62 | ); 63 | startI = -1; 64 | } else { 65 | arr.push(BigInt(byte)); 66 | } 67 | continue; 68 | } 69 | if (startI === -1) { 70 | startI = i; 71 | } 72 | } 73 | 74 | for (let i = 0; i < arr.length / 2; i++) { 75 | const keyI = i * 2; 76 | const key = arr[keyI]; 77 | // split the edicts data, edict has different data format 78 | if (Number(key) === Tag.Body) { 79 | this.edicts = arr.slice(keyI + 1); 80 | break; 81 | } 82 | 83 | const valueI = i * 2 + 1; 84 | if (valueI >= arr.length) { 85 | throw new Error("Buffer length is not valid"); 86 | } 87 | const value = arr[valueI]; 88 | const mapValue = this.tagMap.get(Number(key)); 89 | 90 | if (mapValue) { 91 | this.tagMap.set(Number(key), mapValue.concat(value)); 92 | continue; 93 | } 94 | this.tagMap.set(Number(key), [value]); 95 | } 96 | } 97 | 98 | getValue( 99 | tag: Tag, 100 | valueType: ValueType, 101 | index: number = 0, 102 | ): Varuint | undefined { 103 | const valueArr = this.tagMap.get(tag); 104 | if (!valueArr) { 105 | return undefined; 106 | } 107 | const value = valueArr[index]; 108 | 109 | switch (valueType) { 110 | case ValueType.U8: 111 | return new U8(value); 112 | case ValueType.U16: 113 | return new U16(value); 114 | case ValueType.U32: 115 | return new U32(value); 116 | case ValueType.U64: 117 | return new U64(value); 118 | case ValueType.U128: 119 | return new U128(value); 120 | } 121 | } 122 | 123 | private pushVaruint(varuint: Varuint) { 124 | const bytes = varuint.toVaruint(); 125 | for (let i = 0; i < bytes.length; i += 1) { 126 | this.payloads.push(bytes[i]); 127 | } 128 | } 129 | 130 | encodeTagPush(tag: Tag, ...ns: (Varuint | undefined)[]) { 131 | for (let i = 0; i < ns.length; i++) { 132 | const n = ns[i]; 133 | 134 | if (n === undefined) { 135 | continue; 136 | } 137 | 138 | this.payloads.push(tag); 139 | this.pushVaruint(n); 140 | } 141 | } 142 | 143 | encodeMultiplePush(ns: (Varuint | undefined)[]) { 144 | if (!ns.length) { 145 | return; 146 | } 147 | 148 | for (let i = 0; i < ns.length; i++) { 149 | const n = ns[i]; 150 | 151 | if (n === undefined) { 152 | continue; 153 | } 154 | this.pushVaruint(n); 155 | } 156 | } 157 | 158 | toBuffer() { 159 | return Buffer.from(this.payloads); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/example.ts: -------------------------------------------------------------------------------- 1 | import { U128, U32, U64 } from "big-varuint-js"; 2 | import { Runestone } from "./Runestone"; 3 | import { RuneId } from "./RuneId"; 4 | import { SpacedRune } from "./SpacedRune"; 5 | import { Symbol } from "./Symbol"; 6 | 7 | function exampleEncode() { 8 | const spacedRune = SpacedRune.fromString("RUNESTONE.COIN"); 9 | 10 | const runestone = new Runestone({ 11 | edicts: [ 12 | { 13 | id: new RuneId(new U64(10n), new U32(1n)), 14 | amount: new U128(23n), 15 | output: new U32(23n), 16 | }, 17 | { 18 | id: new RuneId(new U64(10n), new U32(1n)), 19 | amount: new U128(1111n), 20 | output: new U32(2222n), 21 | }, 22 | ], 23 | mint: new RuneId(new U64(232390482390843242n), new U32(19823742n)), 24 | pointer: new U32(1212342342n), 25 | etching: { 26 | rune: spacedRune.rune, 27 | spacers: spacedRune.spacers, 28 | premine: new U128(232390482390843242n), 29 | symbol: Symbol.fromString("R"), 30 | terms: { 31 | amount: new U128(232323232323232390482390843242n), 32 | cap: new U128(532390482390843242n), 33 | height: { 34 | start: new U64(1n), 35 | end: new U64(1n), 36 | }, 37 | offset: { 38 | start: new U64(2n), 39 | end: new U64(2n), 40 | }, 41 | }, 42 | }, 43 | }); 44 | 45 | const buffer = runestone.enchiper(); 46 | console.log({ 47 | buffer, 48 | commitBuffer: runestone.etching?.rune?.commitBuffer(), 49 | }); 50 | // *the script has no "OP_RETURN OP_13 [bytes_length]" preffix, please add it manually if you want to put it inside the tx 51 | // - hex: 0x6a 0x5d [bytes_length] ... 52 | // - bitcoinjs-lib: bitcoin.script.compile([bitcoin.opcodes.OP_RETURN, bitcoin.opcodes.OP_13, buffer]) 53 | // output: 54 | // { 55 | // buffer: Buffer(92) [ 2, 3, 4, 219, 230, 200, 158, 172, 179, 226, 247, 24, 3, 128, 2, 6, 234, 254, 217, 192, 153, 189, 231, 156, 3, 5, 1, 10, 234, 254, 137, 250, 190, 157, 153, 179, 175, 152, 152, 234, 234, 93, 8, 234, 254, 209, 133, 171, 202, 219, 177, 7, 12, 1, 14, 1, 16, 2, 18, 2, 22, 198, 192, 139, 194, 4, 20, 234, 254, 217, 192, 153, 189, 231, 156, 3, 20, 254, 248, 185, 9, 0, 10, 1, 23, 23, 0, 0, 215, 8, 174, 17 ], 56 | // commitBuffer: Buffer(8) [ 91, 51, 210, 195, 154, 137, 239, 24 ], 57 | // } 58 | 59 | console.log(JSON.stringify(runestone)); 60 | // output: 61 | // {"edicts":[{"id":{"block":"10","tx":"1"},"amount":"23","output":"23"},{"id":{"block":"10","tx":"1"},"amount":"1111","output":"2222"}],"etching":{"rune":"RUNESTONECOIN","spacers":"256","premine":"232390482390843242","symbol":"R","terms":{"amount":"232323232323232390482390843242","cap":"532390482390843242","height":{"start":"1","end":"1"},"offset":{"start":"2","end":"2"}}},"mint":{"block":"232390482390843242","tx":"19823742"},"pointer":"1212342342"} 62 | } 63 | 64 | function exampleDecodeFromTxBuffer() { 65 | // https://mempool.space/testnet/tx/0201fbf76be120e0245f95011e9e92bf588d9d5888b0c1eb62823312a3a86a87 66 | const txBuffer = Buffer.from( 67 | // this is the pure output buffer 68 | "020304d5b59d81cfa3a0f520012603a002055806b89cde93898eca010ae8070888a4011601", 69 | "hex", 70 | ); 71 | console.log({ txBuffer }); 72 | 73 | const runestone = Runestone.dechiper(txBuffer); 74 | const buffer = runestone.enchiper(); 75 | console.log({ buffer }); 76 | // output: 77 | // { 78 | // buffer: Buffer(37) [ 2, 3, 4, 213, 181, 157, 129, 207, 163, 160, 245, 32, 1, 38, 3, 160, 2, 6, 184, 156, 222, 147, 137, 142, 202, 1, 5, 88, 10, 232, 7, 8, 136, 164, 1, 22, 1 ], 79 | // } 80 | console.log(JSON.stringify(runestone)); 81 | // output: 82 | // {"edicts":[],"etching":{"divisibility":"38","premine":"888888888888888","rune":"XVERSEFORTEST","spacers":"288","symbol":"X","terms":{"amount":"1000","cap":"21000","height":{},"offset":{}}},"pointer":"1"} 83 | 84 | const spacedRune = new SpacedRune( 85 | runestone.etching?.rune!, 86 | runestone.etching?.spacers!, 87 | ); 88 | console.log(spacedRune.toString()); 89 | // output: XVERSE•FOR•TEST 90 | } 91 | 92 | function main() { 93 | exampleEncode(); 94 | exampleDecodeFromTxBuffer(); 95 | } 96 | 97 | main(); 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Bitcoin ordinals runestone JS implementation 2 | 3 | This implementation is based on ord 0.17.1, \*use this as a reference only 4 | 5 | ## Key Features 6 | 7 | - enchiper(encode) 8 | - dechiper(decode) 9 | 10 | ## Example 11 | 12 | ### Create Rune Coin + Inscription 13 | 14 | ``` 15 | https://github.com/joundy/bitcoin-rune-creator-js 16 | ``` 17 | 18 | ### enchiper 19 | 20 | ``` 21 | function exampleEncode() { 22 | const spacedRune = SpacedRune.fromString("RUNESTONE.COIN"); 23 | 24 | const runestone = new Runestone({ 25 | edicts: [ 26 | { 27 | id: new RuneId(new U64(10n), new U32(1n)), 28 | amount: new U128(23n), 29 | output: new U32(23n), 30 | }, 31 | { 32 | id: new RuneId(new U64(10n), new U32(1n)), 33 | amount: new U128(1111n), 34 | output: new U32(2222n), 35 | }, 36 | ], 37 | mint: new RuneId(new U64(232390482390843242n), new U32(19823742n)), 38 | pointer: new U32(1212342342n), 39 | etching: { 40 | rune: spacedRune.rune, 41 | spacers: spacedRune.spacers, 42 | premine: new U128(232390482390843242n), 43 | symbol: Symbol.fromString("R"), 44 | terms: { 45 | amount: new U128(232323232323232390482390843242n), 46 | cap: new U128(532390482390843242n), 47 | height: { 48 | start: new U64(1n), 49 | end: new U64(1n), 50 | }, 51 | offset: { 52 | start: new U64(2n), 53 | end: new U64(2n), 54 | }, 55 | }, 56 | }, 57 | }); 58 | 59 | const buffer = runestone.enchiper(); 60 | console.log({ 61 | buffer, 62 | commitBuffer: runestone.etching?.rune?.commitBuffer(), 63 | }); 64 | // *the script has no "OP_RETURN OP_13 [bytes_length]" preffix, please add it manually if you want to put it inside the tx 65 | // - hex: 0x6a 0x5d [bytes_length] ... 66 | // - bitcoinjs-lib: bitcoin.script.compile([bitcoin.opcodes.OP_RETURN, bitcoin.opcodes.OP_13, buffer]) 67 | // output: 68 | // { 69 | // buffer: Buffer(92) [ 2, 3, 4, 219, 230, 200, 158, 172, 179, 226, 247, 24, 3, 128, 2, 6, 234, 254, 217, 192, 153, 189, 231, 156, 3, 5, 1, 10, 234, 254, 137, 250, 190, 157, 153, 179, 175, 152, 152, 234, 234, 93, 8, 234, 254, 209, 133, 171, 202, 219, 177, 7, 12, 1, 14, 1, 16, 2, 18, 2, 22, 198, 192, 139, 194, 4, 20, 234, 254, 217, 192, 153, 189, 231, 156, 3, 20, 254, 248, 185, 9, 0, 10, 1, 23, 23, 0, 0, 215, 8, 174, 17 ], 70 | // commitBuffer: Buffer(8) [ 91, 51, 210, 195, 154, 137, 239, 24 ], 71 | // } 72 | 73 | console.log(JSON.stringify(runestone)); 74 | // output: 75 | // {"edicts":[{"id":{"block":"10","tx":"1"},"amount":"23","output":"23"},{"id":{"block":"10","tx":"1"},"amount":"1111","output":"2222"}],"etching":{"rune":"RUNESTONECOIN","spacers":"256","premine":"232390482390843242","symbol":"R","terms":{"amount":"232323232323232390482390843242","cap":"532390482390843242","height":{"start":"1","end":"1"},"offset":{"start":"2","end":"2"}}},"mint":{"block":"232390482390843242","tx":"19823742"},"pointer":"1212342342"} 76 | } 77 | ``` 78 | 79 | ### dechiper from tx 80 | 81 | ``` 82 | function exampleDecodeFromTxBuffer() { 83 | // https://mempool.space/testnet/tx/0201fbf76be120e0245f95011e9e92bf588d9d5888b0c1eb62823312a3a86a87 84 | const txBuffer = Buffer.from( 85 | // this is the pure output buffer 86 | "020304d5b59d81cfa3a0f520012603a002055806b89cde93898eca010ae8070888a4011601", 87 | "hex", 88 | ); 89 | console.log({ txBuffer }); 90 | 91 | const runestone = Runestone.dechiper(txBuffer); 92 | const buffer = runestone.enchiper(); 93 | console.log({ buffer }); 94 | // output: 95 | // { 96 | // buffer: Buffer(37) [ 2, 3, 4, 213, 181, 157, 129, 207, 163, 160, 245, 32, 1, 38, 3, 160, 2, 6, 184, 156, 222, 147, 137, 142, 202, 1, 5, 88, 10, 232, 7, 8, 136, 164, 1, 22, 1 ], 97 | // } 98 | console.log(JSON.stringify(runestone)); 99 | // output: 100 | // {"edicts":[],"etching":{"divisibility":"38","premine":"888888888888888","rune":"XVERSEFORTEST","spacers":"288","symbol":"X","terms":{"amount":"1000","cap":"21000","height":{},"offset":{}}},"pointer":"1"} 101 | 102 | const spacedRune = new SpacedRune( 103 | runestone.etching?.rune!, 104 | runestone.etching?.spacers!, 105 | ); 106 | console.log(spacedRune.toString()); 107 | // output: XVERSE•FOR•TEST 108 | } 109 | 110 | function main() { 111 | exampleEncode(); 112 | exampleDecodeFromTxBuffer(); 113 | } 114 | ``` 115 | -------------------------------------------------------------------------------- /src/Runestone.ts: -------------------------------------------------------------------------------- 1 | import { U128, U32, U64, U8 } from "big-varuint-js"; 2 | import { Edict, Etching, RunestoneParams } from "./types"; 3 | import { Tag, ValueType } from "./Tag"; 4 | import { Flag, FlagEnum } from "./Flag"; 5 | import { TagPayload } from "./Tag"; 6 | import { RuneId } from "./RuneId"; 7 | import { Rune } from "./Rune"; 8 | import { Symbol } from "./Symbol"; 9 | 10 | export class Runestone { 11 | readonly edicts: Edict[]; 12 | readonly etching?: Etching; 13 | readonly mint?: RuneId; 14 | readonly pointer?: U32; 15 | 16 | constructor(runestone: RunestoneParams) { 17 | this.edicts = runestone.edicts; 18 | this.etching = runestone.etching; 19 | this.mint = runestone.mint; 20 | this.pointer = runestone.pointer; 21 | } 22 | 23 | static dechiper(buff: Buffer) { 24 | const tagPayload = new TagPayload(buff); 25 | tagPayload.decode(); 26 | 27 | let etching: Etching | undefined; 28 | const flagP = tagPayload.getValue(Tag.Flags, ValueType.U8) as U8; 29 | if (flagP) { 30 | const flag = new Flag(flagP); 31 | if (flag.hasFlag(FlagEnum.Etching)) { 32 | etching = { 33 | divisibility: tagPayload.getValue( 34 | Tag.Divisibility, 35 | ValueType.U8, 36 | ) as U8, 37 | premine: tagPayload.getValue(Tag.Premine, ValueType.U128) as U128, 38 | rune: new Rune(tagPayload.getValue(Tag.Rune, ValueType.U128) as U128), 39 | spacers: tagPayload.getValue(Tag.Spacers, ValueType.U32) as U32, 40 | symbol: tagPayload.getValue(Tag.Symbol, ValueType.U8) 41 | ? new Symbol(tagPayload.getValue(Tag.Symbol, ValueType.U8) as U8) 42 | : undefined, 43 | terms: flag.hasFlag(FlagEnum.Terms) 44 | ? { 45 | amount: tagPayload.getValue(Tag.Amount, ValueType.U128) as U128, 46 | cap: tagPayload.getValue(Tag.Cap, ValueType.U128) as U128, 47 | height: { 48 | start: tagPayload.getValue( 49 | Tag.HeightStart, 50 | ValueType.U64, 51 | ) as U64, 52 | end: tagPayload.getValue(Tag.HeightEnd, ValueType.U64) as U64, 53 | }, 54 | offset: { 55 | start: tagPayload.getValue( 56 | Tag.OffsetStart, 57 | ValueType.U64, 58 | ) as U64, 59 | end: tagPayload.getValue(Tag.OffsetEnd, ValueType.U64) as U64, 60 | }, 61 | } 62 | : undefined, 63 | }; 64 | } 65 | } 66 | const pointer = tagPayload.getValue(Tag.Pointer, ValueType.U32) as U32; 67 | 68 | const runeIdBlock = tagPayload.getValue(Tag.Mint, ValueType.U64, 0) as U64; 69 | const runeIdTx = tagPayload.getValue(Tag.Mint, ValueType.U64, 1) as U64; 70 | let mint; 71 | if (runeIdBlock && runeIdTx) { 72 | mint = new RuneId(runeIdBlock, runeIdTx); 73 | } 74 | 75 | const edicts: Edict[] = []; 76 | const edictsP = tagPayload.edicts; 77 | if (edictsP.length) { 78 | if (edictsP.length && edictsP.length % 4) { 79 | throw new Error("Edict data length is not valid"); 80 | } 81 | 82 | let next = new RuneId(new U64(0n), new U32(0n)); 83 | for (let i = 0; i < edictsP.length / 4; i++) { 84 | const eI = i * 4; 85 | 86 | const runeId = next.next( 87 | new RuneId(new U64(edictsP[eI]), new U32(edictsP[eI + 1])), 88 | ); 89 | const amount = edictsP[eI + 2]; 90 | const output = edictsP[eI + 3]; 91 | 92 | edicts.push({ 93 | id: runeId, 94 | amount: new U128(amount), 95 | output: new U32(output), 96 | }); 97 | 98 | next = runeId; 99 | } 100 | } 101 | 102 | return new Runestone({ 103 | etching: etching, 104 | edicts, 105 | pointer, 106 | mint, 107 | }); 108 | } 109 | 110 | enchiper(): Buffer { 111 | const tag = new TagPayload(); 112 | 113 | if (this.etching !== undefined) { 114 | const etching = this.etching; 115 | 116 | const flag = new Flag(new U8(0n)); 117 | flag.set(FlagEnum.Etching); 118 | if (etching.terms) { 119 | flag.set(FlagEnum.Terms); 120 | } 121 | tag.encodeTagPush(Tag.Flags, flag.toValue()); 122 | 123 | tag.encodeTagPush(Tag.Rune, etching.rune?.rune); 124 | tag.encodeTagPush(Tag.Divisibility, etching.divisibility); 125 | tag.encodeTagPush(Tag.Spacers, etching.spacers); 126 | tag.encodeTagPush(Tag.Premine, etching.premine); 127 | tag.encodeTagPush(Tag.Symbol, etching.symbol?.symbol); 128 | 129 | if (etching.terms) { 130 | const terms = etching.terms; 131 | 132 | tag.encodeTagPush(Tag.Amount, terms.amount); 133 | tag.encodeTagPush(Tag.Cap, terms.cap); 134 | 135 | if (terms.height) { 136 | tag.encodeTagPush(Tag.HeightStart, terms.height.start); 137 | tag.encodeTagPush(Tag.HeightEnd, terms.height.end); 138 | } 139 | 140 | if (terms.offset) { 141 | tag.encodeTagPush(Tag.OffsetStart, terms.offset.start); 142 | tag.encodeTagPush(Tag.OffsetEnd, terms.offset.end); 143 | } 144 | } 145 | } 146 | 147 | tag.encodeTagPush(Tag.Pointer, this.pointer); 148 | if (this.mint !== undefined) { 149 | tag.encodeTagPush(Tag.Mint, this.mint.block, this.mint.tx); 150 | } 151 | 152 | if (this.edicts.length) { 153 | tag.payloads.push(Tag.Body); 154 | 155 | this.edicts.sort((a, b) => { 156 | return Number( 157 | a.id.block.toValue() - b.id.block.toValue() || 158 | a.id.tx.toValue() - b.id.tx.toValue(), 159 | ); 160 | }); 161 | 162 | let delta = new RuneId(new U64(0n), new U32(0n)); 163 | for (let i = 0; i < this.edicts.length; i += 1) { 164 | const edict = this.edicts[i]; 165 | 166 | const runeId = delta.delta(edict.id); 167 | tag.encodeMultiplePush([ 168 | runeId.block, 169 | runeId.tx, 170 | edict.amount, 171 | edict.output, 172 | ]); 173 | 174 | delta = edict.id; 175 | } 176 | } 177 | 178 | return tag.toBuffer(); 179 | } 180 | } 181 | --------------------------------------------------------------------------------