├── .cargo └── config.toml ├── .codecov.yml ├── .dockerignore ├── .gitattributes ├── .github ├── CONTRIBUTING.md └── workflows │ ├── ci.yaml │ ├── pr-mutants.yml │ └── vercel.yml ├── .gitignore ├── .releaserc ├── .vscode ├── Indexer.toml ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── api ├── .editorconfig ├── .gitignore ├── ordinals │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .nvmrc │ ├── README.md │ ├── client │ │ ├── typescript.json │ │ └── typescript │ │ │ ├── .gitignore │ │ │ ├── .swagger-codegen-ignore │ │ │ ├── .swagger-codegen │ │ │ └── VERSION │ │ │ ├── README.md │ │ │ ├── api.ts │ │ │ ├── api_test.spec.ts │ │ │ ├── configuration.ts │ │ │ ├── custom.d.ts │ │ │ ├── git_push.sh │ │ │ ├── index.ts │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── tsconfig.json │ ├── docs │ │ ├── feature-guides │ │ │ └── rate-limiting.md │ │ └── overview.md │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── @types │ │ │ └── fastify │ │ │ │ └── index.d.ts │ │ ├── api │ │ │ ├── init.ts │ │ │ ├── routes │ │ │ │ ├── brc20.ts │ │ │ │ ├── inscriptions.ts │ │ │ │ ├── sats.ts │ │ │ │ ├── stats.ts │ │ │ │ └── status.ts │ │ │ ├── schemas.ts │ │ │ └── util │ │ │ │ ├── cache.ts │ │ │ │ ├── helpers.ts │ │ │ │ └── ordinal-satoshi.ts │ │ ├── env.ts │ │ ├── index.ts │ │ ├── metrics │ │ │ └── metrics.ts │ │ └── pg │ │ │ ├── brc20 │ │ │ ├── brc20-pg-store.ts │ │ │ ├── helpers.ts │ │ │ └── types.ts │ │ │ ├── counts │ │ │ ├── counts-pg-store.ts │ │ │ ├── helpers.ts │ │ │ └── types.ts │ │ │ ├── helpers.ts │ │ │ ├── pg-store.ts │ │ │ └── types.ts │ ├── tests │ │ ├── api │ │ │ ├── cache.test.ts │ │ │ ├── inscriptions.test.ts │ │ │ ├── ordinal-satoshi.test.ts │ │ │ ├── sats.test.ts │ │ │ ├── stats.test.ts │ │ │ └── status.test.ts │ │ ├── brc-20 │ │ │ └── api.test.ts │ │ ├── helpers.ts │ │ └── setup.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── util │ │ └── openapi-generator.ts ├── package-lock.json ├── package.json └── runes │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .nvmrc │ ├── README.md │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── @types │ │ └── fastify │ │ │ └── index.d.ts │ ├── api │ │ ├── init.ts │ │ ├── routes │ │ │ ├── addresses.ts │ │ │ ├── blocks.ts │ │ │ ├── etchings.ts │ │ │ ├── status.ts │ │ │ └── transactions.ts │ │ ├── schemas.ts │ │ └── util │ │ │ ├── cache.ts │ │ │ └── helpers.ts │ ├── env.ts │ ├── index.ts │ ├── metrics │ │ └── metrics.ts │ └── pg │ │ ├── pg-store.ts │ │ └── types.ts │ ├── tests │ ├── api │ │ └── api.test.ts │ ├── helpers.ts │ └── setup.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── util │ └── openapi-generator.ts ├── components ├── bitcoind │ ├── Cargo.toml │ └── src │ │ ├── indexer │ │ ├── bitcoin │ │ │ ├── cursor.rs │ │ │ ├── fixtures │ │ │ │ └── blocks_json │ │ │ │ │ └── 279671.json │ │ │ ├── mod.rs │ │ │ ├── pipeline.rs │ │ │ └── tests.rs │ │ ├── chain_segment.rs │ │ ├── fork_scratch_pad.rs │ │ ├── mod.rs │ │ └── tests │ │ │ ├── helpers │ │ │ ├── accounts.rs │ │ │ ├── bitcoin_blocks.rs │ │ │ ├── bitcoin_shapes.rs │ │ │ ├── mod.rs │ │ │ └── transactions.rs │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── observer │ │ ├── mod.rs │ │ └── zmq.rs │ │ ├── types │ │ ├── bitcoin.rs │ │ ├── mod.rs │ │ ├── ordinals.rs │ │ ├── processors.rs │ │ └── rosetta.rs │ │ └── utils │ │ ├── bitcoind.rs │ │ └── mod.rs ├── cli │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── cli │ │ ├── commands.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ └── main.rs ├── config │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── generator.rs │ │ ├── lib.rs │ │ └── toml.rs ├── ord │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── chain.rs │ │ ├── charm.rs │ │ ├── decimal_sat.rs │ │ ├── degree.rs │ │ ├── envelope.rs │ │ ├── epoch.rs │ │ ├── height.rs │ │ ├── inscription.rs │ │ ├── inscription_id.rs │ │ ├── lib.rs │ │ ├── media.rs │ │ ├── rarity.rs │ │ ├── sat.rs │ │ ├── sat_point.rs │ │ └── tag.rs ├── ordinals │ ├── Cargo.toml │ └── src │ │ ├── core │ │ ├── meta_protocols │ │ │ ├── brc20 │ │ │ │ ├── brc20_pg.rs │ │ │ │ ├── cache.rs │ │ │ │ ├── index.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── models │ │ │ │ │ ├── db_operation.rs │ │ │ │ │ ├── db_token.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── parser.rs │ │ │ │ ├── test_utils.rs │ │ │ │ └── verifier.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── pipeline │ │ │ ├── mod.rs │ │ │ └── processors │ │ │ │ ├── block_archiving.rs │ │ │ │ ├── inscription_indexing.rs │ │ │ │ └── mod.rs │ │ ├── protocol │ │ │ ├── inscription_parsing.rs │ │ │ ├── inscription_sequencing.rs │ │ │ ├── mod.rs │ │ │ ├── satoshi_numbering.rs │ │ │ ├── satoshi_tracking.rs │ │ │ └── sequence_cursor.rs │ │ └── test_builders.rs │ │ ├── db │ │ ├── blocks.rs │ │ ├── mod.rs │ │ ├── models │ │ │ ├── db_current_location.rs │ │ │ ├── db_inscription.rs │ │ │ ├── db_inscription_parent.rs │ │ │ ├── db_inscription_recursion.rs │ │ │ ├── db_location.rs │ │ │ ├── db_satoshi.rs │ │ │ └── mod.rs │ │ └── ordinals_pg.rs │ │ ├── lib.rs │ │ └── utils │ │ ├── mod.rs │ │ └── monitoring.rs ├── postgres │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── types │ │ ├── mod.rs │ │ ├── pg_bigint_u32.rs │ │ ├── pg_numeric_u128.rs │ │ ├── pg_numeric_u64.rs │ │ └── pg_smallint_u8.rs │ │ └── utils.rs └── runes │ ├── Cargo.toml │ └── src │ ├── db │ ├── cache │ │ ├── db_cache.rs │ │ ├── index_cache.rs │ │ ├── input_rune_balance.rs │ │ ├── mod.rs │ │ ├── transaction_cache.rs │ │ ├── transaction_location.rs │ │ └── utils.rs │ ├── index.rs │ ├── mod.rs │ └── models │ │ ├── db_balance_change.rs │ │ ├── db_ledger_entry.rs │ │ ├── db_ledger_operation.rs │ │ ├── db_rune.rs │ │ ├── db_supply_change.rs │ │ └── mod.rs │ ├── lib.rs │ └── utils │ ├── mod.rs │ └── monitoring.rs ├── dockerfiles ├── components │ ├── bitcoin-indexer.dockerfile │ ├── ordinals-api.dockerfile │ └── runes-api.dockerfile └── docker-compose.dev.postgres.yml ├── migrations ├── ordinals-brc20 │ ├── V1__tokens.sql │ ├── V2__operations.sql │ ├── V3__balances.sql │ ├── V4__counts_by_operation.sql │ ├── V5__counts_by_address_operation.sql │ ├── V6__operations_to_address_index.sql │ └── V7__balances_history.sql ├── ordinals │ ├── V10__counts_by_sat_rarity.sql │ ├── V11__counts_by_type.sql │ ├── V12__counts_by_address.sql │ ├── V13__counts_by_genesis_address.sql │ ├── V14__counts_by_recursive.sql │ ├── V15__inscription_parents.sql │ ├── V16__inscription_charms.sql │ ├── V17__unbound_inscription_sequence.sql │ ├── V18__chain_tip_block_hash.sql │ ├── V1__satoshis.sql │ ├── V2__inscriptions.sql │ ├── V3__locations.sql │ ├── V4__current_locations.sql │ ├── V5__inscription_transfers.sql │ ├── V6__chain_tip.sql │ ├── V7__inscription_recursions.sql │ ├── V8__counts_by_block.sql │ └── V9__counts_by_mime_type.sql └── runes │ ├── V1__runes.sql │ ├── V2__supply_changes.sql │ ├── V3__ledger.sql │ └── V4__balance_changes.sql ├── ordhook.code-workspace ├── rust-toolchain.toml ├── scripts └── run-tests.sh └── vercel.json /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | bitcoin-indexer-install = "install --path components/cli --locked --force" 3 | bitcoin-indexer-fmt = "fmt -- --config group_imports=StdExternalCrate,imports_granularity=Crate" 4 | bitcoin-indexer-clippy = "clippy --tests --all-features --all-targets -- -A clippy::too_many_arguments -A clippy::needless_return -A clippy::type_complexity -A clippy::ptr_arg" 5 | bitcoin-indexer-clippy-cli = "clippy --tests --all-features --all-targets --message-format=short -- -A clippy::too_many_arguments -A clippy::needless_return -A clippy::type_complexity -A clippy::ptr_arg -D warnings" 6 | 7 | [env] 8 | RUST_TEST_THREADS = "1" 9 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "components/ord/" 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /dockerfiles/ 3 | node_modules/ 4 | tmp/ 5 | .vercel 6 | .github 7 | .vscode 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare Clarity files that will always have LF line endings on checkout. 2 | *.clar text eol=lf 3 | 4 | # Denote all files that are truly binary and should not be modified. 5 | *.png binary 6 | *.jpg binary 7 | -------------------------------------------------------------------------------- /.github/workflows/vercel.yml: -------------------------------------------------------------------------------- 1 | name: Vercel 2 | 3 | env: 4 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 5 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 6 | 7 | on: 8 | push: 9 | branches: 10 | - beta 11 | - develop 12 | pull_request: 13 | release: 14 | types: 15 | - published 16 | workflow_dispatch: 17 | 18 | jobs: 19 | openapi-publish: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Use Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: '20' 31 | 32 | - name: Install Vercel CLI 33 | run: npm install --global vercel@latest 34 | 35 | - name: Pull Vercel environment information 36 | run: vercel pull --yes --environment=${{ github.ref_name == 'main' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }} 37 | 38 | - name: Build project artifacts 39 | run: vercel build ${{ github.ref_name == 'main' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }} 40 | 41 | - name: Deploy project artifacts to Vercel 42 | id: deploy 43 | run: vercel ${{ github.ref_name == 'main' && '--prod' || 'deploy' }} --prebuilt --token=${{ secrets.VERCEL_TOKEN }} | awk '{print "deployment_url="$1}' >> $GITHUB_OUTPUT 44 | 45 | - name: Trigger docs.hiro.so deployment 46 | if: github.ref_name == 'main' 47 | run: curl -X POST ${{ secrets.VERCEL_DOCS_DEPLOY_HOOK_URL }} 48 | 49 | - name: Add comment with Vercel deployment URL 50 | if: ${{ github.event_name == 'pull_request' }} 51 | uses: thollander/actions-comment-pull-request@v2 52 | with: 53 | comment_tag: vercel 54 | message: | 55 | Vercel deployment URL: ${{ steps.deploy.outputs.deployment_url }} :rocket: 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | clarinet.code-workspace 4 | history.txt 5 | node_modules 6 | **/node_modules 7 | target 8 | index.node 9 | npm-debug.log* 10 | **/settings/Mainnet.toml 11 | **/settings/Testnet.toml 12 | **/.requirements 13 | **/.cache 14 | **/.build 15 | components/stacks-devnet-js/dist 16 | components/stacks-devnet-js/build 17 | components/chainhook-types-js/dist 18 | *.tar.gz 19 | *.zip 20 | *.rdb 21 | /Ordhook.toml 22 | 23 | /cache/ 24 | ./tests 25 | tmp/ 26 | data 27 | 28 | # Created by https://www.toptal.com/developers/gitignore/api/node 29 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 30 | 31 | ### Node ### 32 | # Logs 33 | logs 34 | *.log 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | lerna-debug.log* 39 | 40 | # Diagnostic reports (https://nodejs.org/api/report.html) 41 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 42 | 43 | # Runtime data 44 | pids 45 | *.pid 46 | *.seed 47 | *.pid.lock 48 | 49 | # Directory for instrumented libs generated by jscoverage/JSCover 50 | lib-cov 51 | 52 | # Coverage directory used by tools like istanbul 53 | coverage 54 | *.lcov 55 | 56 | # nyc test coverage 57 | .nyc_output 58 | 59 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 60 | .grunt 61 | 62 | # Bower dependency directory (https://bower.io/) 63 | bower_components 64 | 65 | # node-waf configuration 66 | .lock-wscript 67 | 68 | # Compiled binary addons (https://nodejs.org/api/addons.html) 69 | build/Release 70 | 71 | # Dependency directories 72 | node_modules/ 73 | jspm_packages/ 74 | 75 | # TypeScript v1 declaration files 76 | typings/ 77 | 78 | # TypeScript cache 79 | *.tsbuildinfo 80 | 81 | # Optional npm cache directory 82 | .npm 83 | 84 | # Optional eslint cache 85 | .eslintcache 86 | 87 | # Microbundle cache 88 | .rpt2_cache/ 89 | .rts2_cache_cjs/ 90 | .rts2_cache_es/ 91 | .rts2_cache_umd/ 92 | 93 | # Optional REPL history 94 | .node_repl_history 95 | 96 | # Output of 'npm pack' 97 | *.tgz 98 | 99 | # Yarn Integrity file 100 | .yarn-integrity 101 | 102 | # dotenv environment variables file 103 | .env 104 | .env.test 105 | 106 | # parcel-bundler cache (https://parceljs.org/) 107 | .cache 108 | 109 | # Next.js build output 110 | .next 111 | 112 | # Nuxt.js build / generate output 113 | .nuxt 114 | dist 115 | 116 | # Gatsby files 117 | .cache/ 118 | # Comment in the public line in if your project uses Gatsby and not Next.js 119 | # https://nextjs.org/blog/next-9-1#public-directory-support 120 | # public 121 | 122 | # vuepress build output 123 | .vuepress/dist 124 | 125 | # Serverless directories 126 | .serverless/ 127 | 128 | # FuseBox cache 129 | .fusebox/ 130 | 131 | # DynamoDB Local files 132 | .dynamodb/ 133 | 134 | # TernJS port file 135 | .tern-port 136 | 137 | # Stores VSCode versions used for testing VSCode extensions 138 | .vscode-test 139 | 140 | # End of https://www.toptal.com/developers/gitignore/api/node 141 | 142 | # Created by https://www.toptal.com/developers/gitignore/api/macos 143 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos 144 | 145 | ### macOS ### 146 | # General 147 | .DS_Store 148 | .AppleDouble 149 | .LSOverride 150 | 151 | # Icon must end with two 152 | Icon 153 | 154 | 155 | # Thumbnails 156 | ._* 157 | 158 | # Files that might appear in the root of a volume 159 | .DocumentRevisions-V100 160 | .fseventsd 161 | .Spotlight-V100 162 | .TemporaryItems 163 | .Trashes 164 | .VolumeIcon.icns 165 | .com.apple.timemachine.donotpresent 166 | 167 | # Directories potentially created on remote AFP share 168 | .AppleDB 169 | .AppleDesktop 170 | Network Trash Folder 171 | Temporary Items 172 | .apdisk 173 | 174 | ### macOS Patch ### 175 | # iCloud generated files 176 | *.icloud 177 | 178 | # End of https://www.toptal.com/developers/gitignore/api/macos 179 | 180 | # Created by https://www.toptal.com/developers/gitignore/api/windows 181 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows 182 | 183 | ### Windows ### 184 | # Windows thumbnail cache files 185 | Thumbs.db 186 | Thumbs.db:encryptable 187 | ehthumbs.db 188 | ehthumbs_vista.db 189 | 190 | # Dump file 191 | *.stackdump 192 | 193 | # Folder config file 194 | [Dd]esktop.ini 195 | 196 | # Recycle Bin used on file shares 197 | $RECYCLE.BIN/ 198 | 199 | # Windows Installer files 200 | *.cab 201 | *.msi 202 | *.msix 203 | *.msm 204 | *.msp 205 | 206 | # Windows shortcuts 207 | *.lnk 208 | 209 | # End of https://www.toptal.com/developers/gitignore/api/windows 210 | 211 | #Added by cargo 212 | 213 | /target 214 | Cargo.lock 215 | 216 | .pnp.* 217 | .yarn/* 218 | !.yarn/patches 219 | !.yarn/plugins 220 | !.yarn/releases 221 | !.yarn/sdks 222 | !.yarn/versions 223 | 224 | *.node 225 | 226 | # Mutation Testing 227 | mutants.out/ 228 | mutants.out.old/ 229 | .vercel 230 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { 5 | "name": "beta", 6 | "channel": "beta", 7 | "prerelease": true 8 | } 9 | ], 10 | "plugins": [ 11 | [ 12 | "@semantic-release/commit-analyzer", 13 | { 14 | "preset": "conventionalcommits" 15 | } 16 | ], 17 | [ 18 | "@semantic-release/release-notes-generator", 19 | { 20 | "preset": "conventionalcommits" 21 | } 22 | ], 23 | [ 24 | "@semantic-release/exec", 25 | { 26 | "prepareCmd": "sed -i -e '1h;2,$H;$!d;g' -e 's@\\[workspace\\.package\\]\\nversion = \"[^\"]*\"@\\[workspace\\.package\\]\\nversion = \"${nextRelease.version}\"@g' Cargo.toml && sed -i -e '1h;2,$H;$!d;g' -e 's@name = \"cli\"\\nversion = \"[^\"]*\"@name = \"cli\"\\nversion = \"${nextRelease.version}\"@g' Cargo.lock" 27 | } 28 | ], 29 | [ 30 | "@semantic-release/github", 31 | { 32 | "successComment": false 33 | } 34 | ], 35 | "@semantic-release/changelog", 36 | "@semantic-release/git" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/Indexer.toml: -------------------------------------------------------------------------------- 1 | [storage] 2 | working_dir = "tmp" 3 | 4 | [metrics] 5 | enabled = true 6 | prometheus_port = 9153 7 | 8 | [ordinals.db] 9 | database = "ordinals" 10 | host = "localhost" 11 | port = 5432 12 | username = "postgres" 13 | password = "postgres" 14 | 15 | [ordinals.meta_protocols.brc20] 16 | enabled = true 17 | lru_cache_size = 10000 18 | 19 | [ordinals.meta_protocols.brc20.db] 20 | database = "brc20" 21 | host = "localhost" 22 | port = 5432 23 | username = "postgres" 24 | password = "postgres" 25 | 26 | [runes] 27 | lru_cache_size = 10000 28 | 29 | [runes.db] 30 | database = "runes" 31 | host = "localhost" 32 | port = 5432 33 | username = "postgres" 34 | password = "postgres" 35 | 36 | [bitcoind] 37 | network = "mainnet" 38 | rpc_url = "http://127.0.0.1:8332" 39 | rpc_username = "devnet" 40 | rpc_password = "devnet" 41 | zmq_url = "tcp://0.0.0.0:18543" 42 | 43 | [resources] 44 | ulimit = 2048 45 | cpu_core_available = 6 46 | memory_available = 16 47 | bitcoind_rpc_threads = 2 48 | bitcoind_rpc_timeout = 15 49 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "lldb", 6 | "request": "launch", 7 | "name": "run: ordinals service", 8 | "cargo": { 9 | "args": ["build", "--bin=bitcoin-indexer", "--package=cli"], 10 | "filter": { 11 | "name": "bitcoin-indexer", 12 | "kind": "bin" 13 | } 14 | }, 15 | "args": [ 16 | "ordinals", 17 | "service", 18 | "start", 19 | "--config-path=${workspaceFolder}/.vscode/Indexer.toml" 20 | ], 21 | "cwd": "${workspaceFolder}" 22 | }, 23 | { 24 | "type": "lldb", 25 | "request": "launch", 26 | "name": "run: runes service", 27 | "cargo": { 28 | "args": ["build", "--bin=bitcoin-indexer", "--package=cli"], 29 | "filter": { 30 | "name": "bitcoin-indexer", 31 | "kind": "bin" 32 | } 33 | }, 34 | "args": [ 35 | "runes", 36 | "service", 37 | "start", 38 | "--config-path=${workspaceFolder}/.vscode/Indexer.toml" 39 | ], 40 | "cwd": "${workspaceFolder}" 41 | }, 42 | { 43 | "type": "node", 44 | "request": "launch", 45 | "name": "run: ordinals-api", 46 | "cwd": "${workspaceFolder}/api/ordinals", 47 | "runtimeArgs": ["-r", "ts-node/register"], 48 | "args": ["${workspaceFolder}/api/ordinals/src/index.ts"], 49 | "outputCapture": "std", 50 | "internalConsoleOptions": "openOnSessionStart", 51 | "envFile": "${workspaceFolder}/api/ordinals/.env", 52 | "env": { 53 | "NODE_ENV": "development", 54 | "TS_NODE_SKIP_IGNORE": "true" 55 | }, 56 | "killBehavior": "polite" 57 | }, 58 | { 59 | "type": "node", 60 | "request": "launch", 61 | "name": "test: ordinals-api", 62 | "program": "${workspaceFolder}/api/ordinals/node_modules/jest/bin/jest", 63 | "cwd": "${workspaceFolder}/api/ordinals/", 64 | "args": ["--testTimeout=3600000", "--runInBand", "--no-cache"], 65 | "outputCapture": "std", 66 | "console": "integratedTerminal", 67 | "preLaunchTask": "npm: testenv:run", 68 | "postDebugTask": "npm: testenv:stop", 69 | "env": { 70 | "PGHOST": "localhost", 71 | "PGUSER": "postgres", 72 | "PGPASSWORD": "postgres" 73 | } 74 | }, 75 | { 76 | "type": "node", 77 | "request": "launch", 78 | "name": "test: ordinals-api (api)", 79 | "program": "${workspaceFolder}/api/ordinals/node_modules/jest/bin/jest", 80 | "cwd": "${workspaceFolder}/api/ordinals/", 81 | "args": [ 82 | "--testTimeout=3600000", 83 | "--runInBand", 84 | "--no-cache", 85 | "${workspaceFolder}/api/ordinals/tests/api/" 86 | ], 87 | "outputCapture": "std", 88 | "console": "integratedTerminal", 89 | "preLaunchTask": "npm: testenv:run", 90 | "postDebugTask": "npm: testenv:stop", 91 | "env": { 92 | "PGHOST": "localhost", 93 | "PGUSER": "postgres", 94 | "PGPASSWORD": "postgres" 95 | } 96 | }, 97 | { 98 | "type": "node", 99 | "request": "launch", 100 | "name": "test: ordinals-api (brc-20)", 101 | "program": "${workspaceFolder}/api/ordinals/node_modules/jest/bin/jest", 102 | "cwd": "${workspaceFolder}/api/ordinals/", 103 | "args": [ 104 | "--testTimeout=3600000", 105 | "--runInBand", 106 | "--no-cache", 107 | "${workspaceFolder}/api/ordinals/tests/brc-20/" 108 | ], 109 | "outputCapture": "std", 110 | "console": "integratedTerminal", 111 | "preLaunchTask": "npm: testenv:run", 112 | "postDebugTask": "npm: testenv:stop", 113 | "env": { 114 | "PGHOST": "localhost", 115 | "PGUSER": "postgres", 116 | "PGPASSWORD": "postgres" 117 | } 118 | }, 119 | { 120 | "type": "node", 121 | "request": "launch", 122 | "name": "test: runes-api", 123 | "program": "${workspaceFolder}/api/runes/node_modules/jest/bin/jest", 124 | "cwd": "${workspaceFolder}/api/runes/", 125 | "args": ["--testTimeout=3600000", "--runInBand", "--no-cache"], 126 | "outputCapture": "std", 127 | "console": "integratedTerminal", 128 | "preLaunchTask": "npm: testenv:run", 129 | "postDebugTask": "npm: testenv:stop", 130 | "env": { 131 | "PGHOST": "localhost", 132 | "PGUSER": "postgres", 133 | "PGPASSWORD": "postgres" 134 | } 135 | } 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rust-lang.rust-analyzer", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnSaveMode": "file", 5 | "rust-analyzer.rustfmt.extraArgs": [ 6 | "--config", 7 | "group_imports=StdExternalCrate,imports_granularity=Crate" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "npm: testenv:run", 6 | "type": "shell", 7 | "command": "npm run testenv:run -- -d", 8 | "isBackground": true, 9 | "options": { 10 | "cwd": "${workspaceFolder}/api/ordinals/", 11 | }, 12 | "problemMatcher": { 13 | "pattern": { 14 | "regexp": ".", 15 | "file": 1, 16 | "location": 2, 17 | "message": 3 18 | }, 19 | "background": { 20 | "activeOnStart": true, 21 | "beginsPattern": ".", 22 | "endsPattern": "." 23 | } 24 | } 25 | }, 26 | { 27 | "label": "npm: testenv:stop", 28 | "type": "shell", 29 | "command": "npm run testenv:stop", 30 | "options": { 31 | "cwd": "${workspaceFolder}/api/ordinals/", 32 | }, 33 | "presentation": { 34 | "echo": true, 35 | "reveal": "silent", 36 | "focus": false, 37 | "panel": "shared", 38 | "showReuseMessage": true, 39 | "clear": false 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "components/bitcoind", 4 | "components/postgres", 5 | "components/cli", 6 | "components/config", 7 | "components/ordinals", 8 | "components/ord", 9 | "components/runes", 10 | ] 11 | default-members = ["components/cli"] 12 | resolver = "2" 13 | 14 | [workspace.dependencies] 15 | bitcoin = "0.32.5" 16 | deadpool-postgres = "0.14.0" 17 | hiro-system-kit = "0.3.4" 18 | refinery = { version = "0.8", features = ["tokio-postgres"] } 19 | tokio = { version = "1.38.1", features = ["full"] } 20 | tokio-postgres = "0.7.10" 21 | prometheus = "0.13.3" 22 | 23 | [workspace.package] 24 | version = "3.0.0" 25 | -------------------------------------------------------------------------------- /api/.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [{*.ts,*.json}] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Build 17 | public/css/main.css 18 | 19 | # Coverage reports 20 | coverage 21 | 22 | # API keys and secrets 23 | .env 24 | 25 | # Dependency directory 26 | node_modules 27 | bower_components 28 | 29 | # Editors 30 | .idea 31 | *.iml 32 | 33 | # OS metadata 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # Ignore built ts files 38 | dist/**/* 39 | 40 | # ignore yarn.lock 41 | yarn.lock 42 | .vercel 43 | .git-info 44 | -------------------------------------------------------------------------------- /api/ordinals/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .eslintrc.js 3 | -------------------------------------------------------------------------------- /api/ordinals/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@stacks/eslint-config', 'prettier'], 4 | overrides: [], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | tsconfigRootDir: __dirname, 8 | project: './tsconfig.json', 9 | ecmaVersion: 2020, 10 | sourceType: 'module', 11 | }, 12 | ignorePatterns: ['*.config.js', 'config/*', '*.mjs', 'tests/*.js', 'client/*'], 13 | plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc', 'prettier'], 14 | rules: { 15 | 'prettier/prettier': 'error', 16 | '@typescript-eslint/no-inferrable-types': 'off', 17 | '@typescript-eslint/camelcase': 'off', 18 | '@typescript-eslint/no-empty-function': 'off', 19 | '@typescript-eslint/no-use-before-define': ['error', 'nofunc'], 20 | '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], 21 | 'no-warning-comments': 'warn', 22 | 'tsdoc/syntax': 'error', 23 | // TODO: Remove this when `any` abi type is fixed. 24 | '@typescript-eslint/no-unsafe-assignment': 'off', 25 | '@typescript-eslint/no-unsafe-member-access': 'off', 26 | '@typescript-eslint/no-unsafe-call': 'off', 27 | '@typescript-eslint/restrict-template-expressions': 'off', 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /api/ordinals/.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript.json: -------------------------------------------------------------------------------- 1 | { 2 | "supportsES6": false, 3 | "npmName": "@hirosystems/ordinals-api-client", 4 | "npmVersion": "1.0.0", 5 | "modelPropertyNaming": "original" 6 | } 7 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/*.js 2 | node_modules 3 | typings 4 | dist 5 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/.swagger-codegen-ignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | package.json 3 | package-lock.json 4 | README.md 5 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/.swagger-codegen/VERSION: -------------------------------------------------------------------------------- 1 | 3.0.42 -------------------------------------------------------------------------------- /api/ordinals/client/typescript/README.md: -------------------------------------------------------------------------------- 1 | ## @hirosystems/ordinals-api-client 2 | 3 | This is a client library for the [Ordinals API](https://github.com/hirosystems/ordinals-api). 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @hirosystems/ordinals-api-client 9 | ``` 10 | 11 | ### Example 12 | 13 | ```typescript 14 | import { Configuration, InscriptionsApi } from "@hirosystems/ordinals-api-client"; 15 | 16 | const config = new Configuration(); 17 | const api = new InscriptionsApi(config); 18 | const result = await api.getInscription("200000") 19 | ``` 20 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/api_test.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ordinals API 3 | * A service that indexes Bitcoin Ordinals data and exposes it via REST API endpoints. 4 | * 5 | * OpenAPI spec version: v0.0.1 6 | * 7 | * 8 | * NOTE: This file is auto generated by the swagger code generator program. 9 | * https://github.com/swagger-api/swagger-codegen.git 10 | * Do not edit the file manually. 11 | */ 12 | 13 | import * as api from "./api" 14 | import { Configuration } from "./configuration" 15 | 16 | const config: Configuration = {} 17 | 18 | describe("InscriptionsApi", () => { 19 | let instance: api.InscriptionsApi 20 | beforeEach(function() { 21 | instance = new api.InscriptionsApi(config) 22 | }); 23 | 24 | test("getInscription", () => { 25 | const id: string = "id_example" 26 | return expect(instance.getInscription(id, {})).resolves.toBe(null) 27 | }) 28 | test("getInscriptionContent", () => { 29 | const id: string = "id_example" 30 | return expect(instance.getInscriptionContent(id, {})).resolves.toBe(null) 31 | }) 32 | test("getInscriptionTransfers", () => { 33 | const id: string = "id_example" 34 | const offset: number = 56 35 | const limit: number = 56 36 | return expect(instance.getInscriptionTransfers(id, offset, limit, {})).resolves.toBe(null) 37 | }) 38 | test("getInscriptions", () => { 39 | const genesis_block: string = "genesis_block_example" 40 | const from_genesis_block_height: string = "from_genesis_block_height_example" 41 | const to_genesis_block_height: string = "to_genesis_block_height_example" 42 | const from_genesis_timestamp: number = 56 43 | const to_genesis_timestamp: number = 56 44 | const from_sat_ordinal: number = 56 45 | const to_sat_ordinal: number = 56 46 | const from_sat_coinbase_height: string = "from_sat_coinbase_height_example" 47 | const to_sat_coinbase_height: string = "to_sat_coinbase_height_example" 48 | const from_number: number = 56 49 | const to_number: number = 56 50 | const id: Array = ["id_example"] 51 | const number: Array = [56] 52 | const output: string = "output_example" 53 | const address: Array = ["address_example"] 54 | const mime_type: Array = ["mime_type_example"] 55 | const rarity: Array = ["rarity_example"] 56 | const offset: number = 56 57 | const limit: number = 56 58 | const order_by: string = "order_by_example" 59 | const order: string = "order_example" 60 | return expect(instance.getInscriptions(genesis_block, from_genesis_block_height, to_genesis_block_height, from_genesis_timestamp, to_genesis_timestamp, from_sat_ordinal, to_sat_ordinal, from_sat_coinbase_height, to_sat_coinbase_height, from_number, to_number, id, number, output, address, mime_type, rarity, offset, limit, order_by, order, {})).resolves.toBe(null) 61 | }) 62 | }) 63 | 64 | describe("SatoshisApi", () => { 65 | let instance: api.SatoshisApi 66 | beforeEach(function() { 67 | instance = new api.SatoshisApi(config) 68 | }); 69 | 70 | test("getSatoshi", () => { 71 | const ordinal: number = 56 72 | return expect(instance.getSatoshi(ordinal, {})).resolves.toBe(null) 73 | }) 74 | }) 75 | 76 | describe("StatusApi", () => { 77 | let instance: api.StatusApi 78 | beforeEach(function() { 79 | instance = new api.StatusApi(config) 80 | }); 81 | 82 | test("getApiStatus", () => { 83 | return expect(instance.getApiStatus({})).resolves.toBe(null) 84 | }) 85 | }) 86 | 87 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/configuration.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | /** 3 | * Ordinals API 4 | * A service that indexes Bitcoin Ordinals data and exposes it via REST API endpoints. 5 | * 6 | * OpenAPI spec version: v0.0.1 7 | * 8 | * 9 | * NOTE: This file is auto generated by the swagger code generator program. 10 | * https://github.com/swagger-api/swagger-codegen.git 11 | * Do not edit the file manually. 12 | */ 13 | 14 | export interface ConfigurationParameters { 15 | apiKey?: string | ((name: string) => string); 16 | username?: string; 17 | password?: string; 18 | accessToken?: string | ((name: string, scopes?: string[]) => string); 19 | basePath?: string; 20 | } 21 | 22 | export class Configuration { 23 | /** 24 | * parameter for apiKey security 25 | * @param name security name 26 | * @memberof Configuration 27 | */ 28 | apiKey?: string | ((name: string) => string); 29 | /** 30 | * parameter for basic security 31 | * 32 | * @type {string} 33 | * @memberof Configuration 34 | */ 35 | username?: string; 36 | /** 37 | * parameter for basic security 38 | * 39 | * @type {string} 40 | * @memberof Configuration 41 | */ 42 | password?: string; 43 | /** 44 | * parameter for oauth2 security 45 | * @param name security name 46 | * @param scopes oauth2 scope 47 | * @memberof Configuration 48 | */ 49 | accessToken?: string | ((name: string, scopes?: string[]) => string); 50 | /** 51 | * override base path 52 | * 53 | * @type {string} 54 | * @memberof Configuration 55 | */ 56 | basePath?: string; 57 | 58 | constructor(param: ConfigurationParameters = {}) { 59 | this.apiKey = param.apiKey; 60 | this.username = param.username; 61 | this.password = param.password; 62 | this.accessToken = param.accessToken; 63 | this.basePath = param.basePath; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'isomorphic-fetch'; 2 | declare module 'url'; -------------------------------------------------------------------------------- /api/ordinals/client/typescript/git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 swagger-petstore-perl "minor update" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | 10 | if [ "$git_user_id" = "" ]; then 11 | git_user_id="GIT_USER_ID" 12 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 13 | fi 14 | 15 | if [ "$git_repo_id" = "" ]; then 16 | git_repo_id="GIT_REPO_ID" 17 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 18 | fi 19 | 20 | if [ "$release_note" = "" ]; then 21 | release_note="Minor update" 22 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 23 | fi 24 | 25 | # Initialize the local directory as a Git repository 26 | git init 27 | 28 | # Adds the files in the local repository and stages them for commit. 29 | git add . 30 | 31 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 32 | git commit -m "$release_note" 33 | 34 | # Sets the new remote 35 | git_remote=`git remote` 36 | if [ "$git_remote" = "" ]; then # git remote not defined 37 | 38 | if [ "$GIT_TOKEN" = "" ]; then 39 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 40 | git remote add origin https://github.com/${git_user_id}/${git_repo_id}.git 41 | else 42 | git remote add origin https://${git_user_id}:${GIT_TOKEN}@github.com/${git_user_id}/${git_repo_id}.git 43 | fi 44 | 45 | fi 46 | 47 | git pull origin master 48 | 49 | # Pushes (Forces) the changes in the local repository up to the remote repository 50 | echo "Git pushing to https://github.com/${git_user_id}/${git_repo_id}.git" 51 | git push origin master 2>&1 | grep -v 'To https' 52 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | /** 3 | * Ordinals API 4 | * A service that indexes Bitcoin Ordinals data and exposes it via REST API endpoints. 5 | * 6 | * OpenAPI spec version: v0.0.1 7 | * 8 | * 9 | * NOTE: This file is auto generated by the swagger code generator program. 10 | * https://github.com/swagger-api/swagger-codegen.git 11 | * Do not edit the file manually. 12 | */ 13 | 14 | export * from "./api"; 15 | export * from "./configuration"; 16 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hirosystems/ordinals-api-client", 3 | "version": "1.0.0", 4 | "description": "Client for @hirosystems/ordinals-api", 5 | "author": "Hiro Systems PBC (https://hiro.so)", 6 | "keywords": [ 7 | "fetch", 8 | "typescript", 9 | "swagger-client", 10 | "@hirosystems/ordinals-api-client" 11 | ], 12 | "license": "GPL-3.0", 13 | "main": "./dist/index.js", 14 | "typings": "./dist/index.d.ts", 15 | "files": [ 16 | "dist/", 17 | "api.ts", 18 | "configuration.ts", 19 | "custom.d.ts", 20 | "index.ts" 21 | ], 22 | "scripts": { 23 | "build": "tsc --outDir dist/", 24 | "test": "jest", 25 | "prepublishOnly": "npm run build" 26 | }, 27 | "dependencies": { 28 | "isomorphic-fetch": "^3.0.0" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^25.2.1", 32 | "@types/node": "^13.13.0", 33 | "@types/babel__core": "7.1.18", 34 | "@types/babel__traverse": "7.14.2", 35 | "jest": "^25.4.0", 36 | "ts-jest": "^25.4.0", 37 | "typescript": "^3.8.3" 38 | }, 39 | "jest": { 40 | "transform": { 41 | "^.+\\.tsx?$": "ts-jest" 42 | }, 43 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 44 | "moduleFileExtensions": [ 45 | "ts", 46 | "tsx", 47 | "js", 48 | "jsx", 49 | "json", 50 | "node" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /api/ordinals/client/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "noImplicitAny": true, 7 | "outDir": "dist", 8 | "rootDir": ".", 9 | "typeRoots": [ 10 | "./node_modules/@types" 11 | ], 12 | "lib": [ 13 | "es6", 14 | "dom" 15 | ] 16 | }, 17 | "exclude": [ 18 | "dist", 19 | "node_modules", 20 | "**/*.spec.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /api/ordinals/docs/feature-guides/rate-limiting.md: -------------------------------------------------------------------------------- 1 | --- 2 | Title: Rate Limiting for Ordinals API 3 | --- 4 | 5 | # Rate Limiting for Ordinals API 6 | 7 | The Rate Limit per Minute(RPM) is applied to all the API endpoints based on the requested token addresses. 8 | 9 | 10 | | **Endpoint** | **API Key Used** | **Rate per minute(RPM) limit** | 11 | |------------------------------|--------------------|--------------------------------| 12 | | api.mainnet.hiro.so/ordinals | No | 50 | 13 | | api.mainnet.hiro.so/ordinals | Yes | 500 | 14 | 15 | If you're interested in obtaining an API key from Hiro, you can generate a free key in the [Hiro Platform](https://platform.hiro.so/). 16 | -------------------------------------------------------------------------------- /api/ordinals/docs/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | Title: Overview 3 | --- 4 | 5 | # Ordinals API Overview 6 | 7 | The Ordinals API provides a service that indexes Bitcoin Ordinals data and offers a REST API to access and query this data. 8 | 9 | > **_NOTE:_** 10 | > 11 | > To explore the detailed documentation for the API endpoints, request and response formats, you can refer to the [OpenAPI specification](https://docs.hiro.so/ordinals). 12 | > 13 | > The source code for this project is available in our [GitHub repository](https://github.com/hirosystems/ordinals-api). 14 | > You can explore the codebase, [contribute](https://docs.hiro.so/contributors-guide), and raise [issues](https://github.com/hirosystems/ordinals-api/issues) or [pull requests](https://github.com/hirosystems/ordinals-api/pulls). 15 | 16 | Here are the key features of the Ordinals API: 17 | 18 | **Ordinal Inscription Ingestion**: 19 | The API helps with the complete ingestion of ordinal inscriptions. 20 | Using our endpoitns, you can retrieve the metadata for a particular inscription, all inscriptions held by a particular address, trading activity for inscriptions, and more. 21 | 22 | **BRC-20 Support**: 23 | The API offers support for BRC-20 tokens, a fungible token standard built on top of ordinal theory. 24 | Retrieve data for a particular BRC-20 token, a user's BRC-20 holdings, marketplace activity, and more. 25 | 26 | **REST JSON Endpoints with ETag Caching**: 27 | The API provides easy-to-use REST endpoints that return responses in JSON format. 28 | It also supports *ETag caching*, which allows you to cache responses based on inscriptions. 29 | This helps optimize performance and reduce unnecessary requests. 30 | 31 | **Auto-Scale Server Configurations**: 32 | The Ordinals API supports three run modes based on the `RUN_MODE` environment variable: 33 | 34 | - `default`: This mode runs all background jobs and the API server. It is suitable for running a single instance of the API. 35 | - `readonly`: Only the API server runs in this mode. It is designed for auto-scaled clusters with multiple `readonly` instances and a single `writeonly` instance. The `writeonly` instance is responsible for populating the database. 36 | - `writeonly`: This mode is used in an auto-scaled environment to consume new inscriptions and push that data to a database. It works in conjunction with multiple `readonly` instances. 37 | -------------------------------------------------------------------------------- /api/ordinals/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ordinals-api", 3 | "description": "REST API that exposes indexed ordinals data", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "Hiro Systems PBC (https://hiro.so)", 7 | "license": "Apache 2.0", 8 | "scripts": { 9 | "build": "rimraf ./dist && tsc --project tsconfig.build.json", 10 | "start": "node dist/src/index.js", 11 | "start-ts": "ts-node ./src/index.ts", 12 | "test": "jest --runInBand", 13 | "test:brc-20": "npm run test -- ./tests/brc-20/", 14 | "test:api": "npm run test -- ./tests/api/", 15 | "lint:eslint": "eslint . --ext .js,.jsx,.ts,.tsx -f unix", 16 | "lint:prettier": "prettier --check src/**/*.ts tests/**/*.ts", 17 | "lint:unused-exports": "ts-unused-exports tsconfig.json --showLineNumber --excludePathsFromReport=util/*", 18 | "generate:openapi": "rimraf ./tmp && node -r ts-node/register ./util/openapi-generator.ts", 19 | "generate:docs": "redoc-cli build --output ./tmp/index.html ./tmp/openapi.yaml", 20 | "generate:git-info": "rimraf .git-info && node_modules/.bin/api-toolkit-git-info", 21 | "generate:vercel": "npm run generate:git-info && npm run generate:openapi && npm run generate:docs", 22 | "testenv:run": "docker compose -f ../../dockerfiles/docker-compose.dev.postgres.yml up", 23 | "testenv:stop": "docker compose -f ../../dockerfiles/docker-compose.dev.postgres.yml down -v -t 0", 24 | "testenv:logs": "docker compose -f ../../dockerfiles/docker-compose.dev.postgres.yml logs -t -f", 25 | "generate:client:typescript": "swagger-codegen generate -i ./tmp/openapi.yaml -l typescript-fetch -o ./client/typescript -c ./client/typescript.json" 26 | }, 27 | "prettier": "@stacks/prettier-config", 28 | "devDependencies": { 29 | "@commitlint/cli": "^17.4.3", 30 | "@commitlint/config-conventional": "^17.4.3", 31 | "@stacks/eslint-config": "^1.2.0", 32 | "@types/jest": "^29.2.4", 33 | "@types/supertest": "^2.0.12", 34 | "@typescript-eslint/eslint-plugin": "^5.46.1", 35 | "@typescript-eslint/parser": "^5.51.0", 36 | "@semantic-release/changelog": "^6.0.3", 37 | "@semantic-release/commit-analyzer": "^10.0.4", 38 | "@semantic-release/git": "^10.0.1", 39 | "babel-jest": "^29.3.1", 40 | "conventional-changelog-conventionalcommits": "^6.1.0", 41 | "eslint": "^8.29.0", 42 | "eslint-plugin-prettier": "^4.2.1", 43 | "eslint-plugin-tsdoc": "^0.2.17", 44 | "husky": "^8.0.3", 45 | "jest": "^29.3.1", 46 | "prettier": "^2.8.1", 47 | "redoc-cli": "^0.13.21", 48 | "rimraf": "^3.0.2", 49 | "semantic-release": "^24.2.1", 50 | "semantic-release-monorepo": "^8.0.2", 51 | "ts-jest": "^29.0.3", 52 | "ts-node": "^10.8.2", 53 | "ts-unused-exports": "^10.0.1", 54 | "typescript": "^4.7.4" 55 | }, 56 | "dependencies": { 57 | "@fastify/cors": "^8.0.0", 58 | "@fastify/formbody": "^7.0.1", 59 | "@fastify/multipart": "^8.3.1", 60 | "@fastify/swagger": "^8.3.1", 61 | "@fastify/type-provider-typebox": "^3.2.0", 62 | "@hirosystems/api-toolkit": "^1.7.2", 63 | "@hirosystems/chainhook-client": "^1.12.0", 64 | "@types/node": "^18.13.0", 65 | "bignumber.js": "^9.1.1", 66 | "bitcoinjs-lib": "^6.1.0", 67 | "env-schema": "^5.2.0", 68 | "fastify": "^4.3.0", 69 | "fastify-metrics": "^10.2.0", 70 | "pino": "^8.10.0", 71 | "postgres": "^3.3.4", 72 | "undici": "^5.28.5" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /api/ordinals/src/@types/fastify/index.d.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify'; 2 | import { PgStore } from '../../pg/pg-store'; 3 | import { Brc20PgStore } from '../../pg/brc20/brc20-pg-store'; 4 | 5 | declare module 'fastify' { 6 | export interface FastifyInstance< 7 | HttpServer = Server, 8 | HttpRequest = IncomingMessage, 9 | HttpResponse = ServerResponse, 10 | Logger = FastifyLoggerInstance, 11 | TypeProvider = FastifyTypeProviderDefault 12 | > { 13 | db: PgStore; 14 | brc20Db: Brc20PgStore; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api/ordinals/src/api/init.ts: -------------------------------------------------------------------------------- 1 | import FastifyCors from '@fastify/cors'; 2 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 3 | import { PINO_LOGGER_CONFIG, isProdEnv } from '@hirosystems/api-toolkit'; 4 | import Fastify, { FastifyPluginAsync } from 'fastify'; 5 | import FastifyMetrics, { IFastifyMetrics } from 'fastify-metrics'; 6 | import { Server } from 'http'; 7 | import { PgStore } from '../pg/pg-store'; 8 | import { Brc20Routes } from './routes/brc20'; 9 | import { InscriptionsRoutes } from './routes/inscriptions'; 10 | import { SatRoutes } from './routes/sats'; 11 | import { StatsRoutes } from './routes/stats'; 12 | import { StatusRoutes } from './routes/status'; 13 | import { Brc20PgStore } from '../pg/brc20/brc20-pg-store'; 14 | 15 | export const Api: FastifyPluginAsync< 16 | Record, 17 | Server, 18 | TypeBoxTypeProvider 19 | > = async fastify => { 20 | await fastify.register(StatusRoutes); 21 | await fastify.register(InscriptionsRoutes); 22 | await fastify.register(SatRoutes); 23 | await fastify.register(StatsRoutes); 24 | await fastify.register(Brc20Routes); 25 | }; 26 | 27 | export async function buildApiServer(args: { db: PgStore; brc20Db: Brc20PgStore }) { 28 | const fastify = Fastify({ 29 | trustProxy: true, 30 | logger: PINO_LOGGER_CONFIG, 31 | }).withTypeProvider(); 32 | 33 | fastify.decorate('db', args.db); 34 | fastify.decorate('brc20Db', args.brc20Db); 35 | if (isProdEnv) { 36 | await fastify.register(FastifyMetrics, { endpoint: null }); 37 | } 38 | await fastify.register(FastifyCors); 39 | await fastify.register(Api, { prefix: '/ordinals/v1' }); 40 | await fastify.register(Api, { prefix: '/ordinals' }); 41 | 42 | return fastify; 43 | } 44 | 45 | export async function buildPromServer(args: { metrics: IFastifyMetrics }) { 46 | const promServer = Fastify({ 47 | trustProxy: true, 48 | logger: PINO_LOGGER_CONFIG, 49 | }); 50 | 51 | promServer.route({ 52 | url: '/metrics', 53 | method: 'GET', 54 | logLevel: 'info', 55 | handler: async (_, reply) => { 56 | await reply.type('text/plain').send(await args.metrics.client.register.metrics()); 57 | }, 58 | }); 59 | 60 | return promServer; 61 | } 62 | -------------------------------------------------------------------------------- /api/ordinals/src/api/routes/sats.ts: -------------------------------------------------------------------------------- 1 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 2 | import { Type } from '@sinclair/typebox'; 3 | import { FastifyPluginCallback } from 'fastify'; 4 | import { Server } from 'http'; 5 | import { 6 | InscriptionResponse, 7 | InvalidSatoshiNumberResponse, 8 | LimitParam, 9 | OffsetParam, 10 | OrdinalParam, 11 | PaginatedResponse, 12 | SatoshiResponse, 13 | } from '../schemas'; 14 | import { OrdinalSatoshi } from '../util/ordinal-satoshi'; 15 | import { DEFAULT_API_LIMIT, parseDbInscriptions } from '../util/helpers'; 16 | 17 | export const SatRoutes: FastifyPluginCallback, Server, TypeBoxTypeProvider> = ( 18 | fastify, 19 | options, 20 | done 21 | ) => { 22 | fastify.get( 23 | '/sats/:ordinal', 24 | { 25 | schema: { 26 | operationId: 'getSatoshi', 27 | summary: 'Satoshi Ordinal', 28 | description: 'Retrieves ordinal information for a single satoshi', 29 | tags: ['Satoshis'], 30 | params: Type.Object({ 31 | ordinal: OrdinalParam, 32 | }), 33 | response: { 34 | 200: SatoshiResponse, 35 | 400: InvalidSatoshiNumberResponse, 36 | }, 37 | }, 38 | }, 39 | async (request, reply) => { 40 | let sat: OrdinalSatoshi; 41 | try { 42 | sat = new OrdinalSatoshi(request.params.ordinal); 43 | } catch (error) { 44 | await reply.code(400).send({ error: 'Invalid satoshi ordinal number' }); 45 | return; 46 | } 47 | const inscriptions = await fastify.db.getInscriptions( 48 | { limit: 1, offset: 0 }, 49 | { sat_ordinal: BigInt(request.params.ordinal) } 50 | ); 51 | await reply.send({ 52 | coinbase_height: sat.blockHeight, 53 | cycle: sat.cycle, 54 | epoch: sat.epoch, 55 | period: sat.period, 56 | offset: sat.offset, 57 | decimal: sat.decimal, 58 | degree: sat.degree, 59 | name: sat.name, 60 | rarity: sat.rarity, 61 | percentile: sat.percentile, 62 | inscription_id: inscriptions.results[0]?.genesis_id, 63 | }); 64 | } 65 | ); 66 | 67 | fastify.get( 68 | '/sats/:ordinal/inscriptions', 69 | { 70 | schema: { 71 | operationId: 'getSatoshiInscriptions', 72 | summary: 'Satoshi Inscriptions', 73 | description: 'Retrieves all inscriptions associated with a single satoshi', 74 | tags: ['Satoshis'], 75 | params: Type.Object({ 76 | ordinal: OrdinalParam, 77 | }), 78 | querystring: Type.Object({ 79 | // Pagination 80 | offset: Type.Optional(OffsetParam), 81 | limit: Type.Optional(LimitParam), 82 | }), 83 | response: { 84 | 200: PaginatedResponse(InscriptionResponse, 'Paginated Satoshi Inscriptions Response'), 85 | 400: InvalidSatoshiNumberResponse, 86 | }, 87 | }, 88 | }, 89 | async (request, reply) => { 90 | let sat: OrdinalSatoshi; 91 | try { 92 | sat = new OrdinalSatoshi(request.params.ordinal); 93 | } catch (error) { 94 | await reply.code(400).send({ error: 'Invalid satoshi ordinal number' }); 95 | return; 96 | } 97 | const limit = request.query.limit ?? DEFAULT_API_LIMIT; 98 | const offset = request.query.offset ?? 0; 99 | const inscriptions = await fastify.db.getInscriptions( 100 | { limit, offset }, 101 | { sat_ordinal: BigInt(sat.ordinal) } 102 | ); 103 | await reply.send({ 104 | limit, 105 | offset, 106 | total: inscriptions.total, 107 | results: parseDbInscriptions(inscriptions.results), 108 | }); 109 | } 110 | ); 111 | 112 | done(); 113 | }; 114 | -------------------------------------------------------------------------------- /api/ordinals/src/api/routes/stats.ts: -------------------------------------------------------------------------------- 1 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 2 | import { Type } from '@sinclair/typebox'; 3 | import { FastifyPluginAsync, FastifyPluginCallback } from 'fastify'; 4 | import { Server } from 'http'; 5 | import { BlockHeightParam, InscriptionsPerBlockResponse, NotFoundResponse } from '../schemas'; 6 | import { handleInscriptionsPerBlockCache } from '../util/cache'; 7 | import { blockParam } from '../util/helpers'; 8 | 9 | const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTypeProvider> = ( 10 | fastify, 11 | options, 12 | done 13 | ) => { 14 | fastify.addHook('preHandler', handleInscriptionsPerBlockCache); 15 | fastify.get( 16 | '/stats/inscriptions', 17 | { 18 | schema: { 19 | operationId: 'getStatsInscriptionCount', 20 | summary: 'Inscription Count per Block', 21 | description: 'Retrieves statistics on the number of inscriptions revealed per block', 22 | tags: ['Statistics'], 23 | querystring: Type.Object({ 24 | from_block_height: Type.Optional(BlockHeightParam), 25 | to_block_height: Type.Optional(BlockHeightParam), 26 | }), 27 | response: { 28 | 200: InscriptionsPerBlockResponse, 29 | 404: NotFoundResponse, 30 | }, 31 | }, 32 | }, 33 | async (request, reply) => { 34 | const inscriptions = await fastify.db.counts.getInscriptionCountPerBlock({ 35 | ...blockParam(request.query.from_block_height, 'from_block'), 36 | ...blockParam(request.query.to_block_height, 'to_block'), 37 | }); 38 | await reply.send({ 39 | results: inscriptions, 40 | }); 41 | } 42 | ); 43 | done(); 44 | }; 45 | 46 | export const StatsRoutes: FastifyPluginAsync< 47 | Record, 48 | Server, 49 | TypeBoxTypeProvider 50 | > = async fastify => { 51 | await fastify.register(IndexRoutes); 52 | }; 53 | -------------------------------------------------------------------------------- /api/ordinals/src/api/routes/status.ts: -------------------------------------------------------------------------------- 1 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 2 | import { FastifyPluginCallback } from 'fastify'; 3 | import { Server } from 'http'; 4 | import { ApiStatusResponse } from '../schemas'; 5 | import { SERVER_VERSION } from '@hirosystems/api-toolkit'; 6 | import { handleInscriptionTransfersCache } from '../util/cache'; 7 | 8 | export const StatusRoutes: FastifyPluginCallback< 9 | Record, 10 | Server, 11 | TypeBoxTypeProvider 12 | > = (fastify, options, done) => { 13 | fastify.addHook('preHandler', handleInscriptionTransfersCache); 14 | fastify.get( 15 | '/', 16 | { 17 | schema: { 18 | operationId: 'getApiStatus', 19 | summary: 'API Status', 20 | description: 'Displays the status of the API', 21 | tags: ['Status'], 22 | response: { 23 | 200: ApiStatusResponse, 24 | }, 25 | }, 26 | }, 27 | async (request, reply) => { 28 | const result = await fastify.db.sqlTransaction(async sql => { 29 | const block_height = await fastify.db.getChainTipBlockHeight(); 30 | const max_inscription_number = await fastify.db.getMaxInscriptionNumber(); 31 | const max_cursed_inscription_number = await fastify.db.getMaxCursedInscriptionNumber(); 32 | return { 33 | server_version: `bitcoin-indexer-ordinals-api ${SERVER_VERSION.tag} (${SERVER_VERSION.branch}:${SERVER_VERSION.commit})`, 34 | status: 'ready', 35 | block_height, 36 | max_inscription_number, 37 | max_cursed_inscription_number, 38 | }; 39 | }); 40 | await reply.send(result); 41 | } 42 | ); 43 | done(); 44 | }; 45 | -------------------------------------------------------------------------------- /api/ordinals/src/api/util/cache.ts: -------------------------------------------------------------------------------- 1 | import { FastifyReply, FastifyRequest } from 'fastify'; 2 | import { InscriptionIdParamCType, InscriptionNumberParamCType } from '../schemas'; 3 | import { CACHE_CONTROL_MUST_REVALIDATE, parseIfNoneMatchHeader } from '@hirosystems/api-toolkit'; 4 | 5 | enum ETagType { 6 | inscriptionsIndex, 7 | inscription, 8 | inscriptionsPerBlock, 9 | } 10 | 11 | export async function handleInscriptionCache(request: FastifyRequest, reply: FastifyReply) { 12 | return handleCache(ETagType.inscription, request, reply); 13 | } 14 | 15 | export async function handleInscriptionTransfersCache( 16 | request: FastifyRequest, 17 | reply: FastifyReply 18 | ) { 19 | return handleCache(ETagType.inscriptionsIndex, request, reply); 20 | } 21 | 22 | export async function handleInscriptionsPerBlockCache( 23 | request: FastifyRequest, 24 | reply: FastifyReply 25 | ) { 26 | return handleCache(ETagType.inscriptionsPerBlock, request, reply); 27 | } 28 | 29 | async function handleCache(type: ETagType, request: FastifyRequest, reply: FastifyReply) { 30 | const ifNoneMatch = parseIfNoneMatchHeader(request.headers['if-none-match']); 31 | let etag: string | undefined; 32 | switch (type) { 33 | case ETagType.inscription: 34 | etag = await getInscriptionLocationEtag(request); 35 | break; 36 | case ETagType.inscriptionsIndex: 37 | etag = await getInscriptionsIndexEtag(request); 38 | break; 39 | case ETagType.inscriptionsPerBlock: 40 | etag = await request.server.db.getInscriptionsPerBlockETag(); 41 | break; 42 | } 43 | if (etag) { 44 | if (ifNoneMatch && ifNoneMatch.includes(etag)) { 45 | await reply.header('Cache-Control', CACHE_CONTROL_MUST_REVALIDATE).code(304).send(); 46 | } else { 47 | void reply.headers({ 'Cache-Control': CACHE_CONTROL_MUST_REVALIDATE, ETag: `"${etag}"` }); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Retrieve the inscriptions's location timestamp as a UNIX epoch so we can use it as the response 54 | * ETag. 55 | * @param request - Fastify request 56 | * @returns Etag string 57 | */ 58 | async function getInscriptionLocationEtag(request: FastifyRequest): Promise { 59 | try { 60 | const components = request.url.split('/'); 61 | do { 62 | const lastElement = components.pop(); 63 | if (lastElement && lastElement.length) { 64 | if (InscriptionIdParamCType.Check(lastElement)) { 65 | return await request.server.db.getInscriptionETag({ genesis_id: lastElement }); 66 | } else if (InscriptionNumberParamCType.Check(parseInt(lastElement))) { 67 | return await request.server.db.getInscriptionETag({ number: lastElement }); 68 | } 69 | } 70 | } while (components.length); 71 | } catch (error) { 72 | return; 73 | } 74 | } 75 | 76 | /** 77 | * Get an ETag based on the last state of all inscriptions. 78 | * @param request - Fastify request 79 | * @returns ETag string 80 | */ 81 | async function getInscriptionsIndexEtag(request: FastifyRequest): Promise { 82 | try { 83 | return await request.server.db.getInscriptionsIndexETag(); 84 | } catch (error) { 85 | return; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /api/ordinals/src/api/util/ordinal-satoshi.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | const HALVING_BLOCKS = 210_000; 4 | const DIFFICULTY_ADJUST_BLOCKS = 2016; 5 | const INITIAL_SUBSIDY = 50; 6 | const SATS_PER_BTC = 100_000_000; 7 | export const SAT_SUPPLY = 2099999997690000; 8 | 9 | export enum SatoshiRarity { 10 | common = 'common', 11 | uncommon = 'uncommon', 12 | rare = 'rare', 13 | epic = 'epic', 14 | legendary = 'legendary', 15 | mythic = 'mythic', 16 | } 17 | 18 | /** 19 | * Ordinal Satoshi calculator. Mostly translated from the original Rust implementation at 20 | * https://github.com/casey/ord/blob/master/src/sat.rs 21 | */ 22 | export class OrdinalSatoshi { 23 | public blockHeight: number; 24 | public cycle: number; 25 | public ordinal: number; 26 | public epoch: number; 27 | public period: number; 28 | public offset: number; 29 | private hour: number; 30 | private minute: number; 31 | private second: number; 32 | private third: number; 33 | 34 | constructor(ordinal: number) { 35 | if (ordinal > SAT_SUPPLY || ordinal < 0) throw Error('Invalid satoshi ordinal number'); 36 | let satAccum = 0; 37 | let subsidy = INITIAL_SUBSIDY; 38 | let epoch = 0; 39 | while (true) { 40 | const satHalvingMax = HALVING_BLOCKS * subsidy * SATS_PER_BTC; 41 | if (satAccum + satHalvingMax > ordinal) { 42 | break; 43 | } 44 | satAccum += satHalvingMax; 45 | subsidy /= 2; 46 | epoch++; 47 | } 48 | const halvingOffset = ordinal - satAccum; 49 | const epochBoundary = epoch * HALVING_BLOCKS; 50 | const exactHeight = halvingOffset / (subsidy * SATS_PER_BTC) + epochBoundary; 51 | 52 | this.ordinal = ordinal; 53 | this.blockHeight = Math.floor(exactHeight); 54 | this.cycle = this.hour = Math.floor(epoch / 6); 55 | this.minute = epochBoundary === 0 ? this.blockHeight : this.blockHeight % epochBoundary; 56 | this.second = this.blockHeight % DIFFICULTY_ADJUST_BLOCKS; 57 | this.third = this.offset = Math.round( 58 | (exactHeight - this.blockHeight) * subsidy * Math.pow(10, 8) 59 | ); 60 | this.epoch = epoch; 61 | this.period = Math.floor(this.blockHeight / DIFFICULTY_ADJUST_BLOCKS); 62 | } 63 | 64 | public get degree(): string { 65 | return `${this.hour}°${this.minute}′${this.second}″${this.third}‴`; 66 | } 67 | 68 | public get decimal(): string { 69 | return `${this.blockHeight}.${this.third}`; 70 | } 71 | 72 | public get name(): string { 73 | let x = SAT_SUPPLY - this.ordinal; 74 | const name: string[] = []; 75 | const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); 76 | while (x > 0) { 77 | const index = Math.floor((x - 1) % 26); 78 | name.push(alphabet[index]); 79 | x = (x - 1) / 26; 80 | } 81 | return name.reverse().join(''); 82 | } 83 | 84 | public get percentile(): string { 85 | const percentile = new BigNumber((this.ordinal / (SAT_SUPPLY - 1)) * 100.0); 86 | return `${percentile.toFixed()}%`; 87 | } 88 | 89 | public get rarity(): SatoshiRarity { 90 | if (this.hour === 0 && this.minute === 0 && this.second === 0 && this.third === 0) { 91 | return SatoshiRarity.mythic; 92 | } 93 | if (this.minute === 0 && this.second === 0 && this.third === 0) { 94 | return SatoshiRarity.legendary; 95 | } 96 | if (this.minute === 0 && this.third === 0) { 97 | return SatoshiRarity.epic; 98 | } 99 | if (this.second === 0 && this.third === 0) { 100 | return SatoshiRarity.rare; 101 | } 102 | if (this.third === 0) { 103 | return SatoshiRarity.uncommon; 104 | } 105 | return SatoshiRarity.common; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /api/ordinals/src/env.ts: -------------------------------------------------------------------------------- 1 | import { Static, Type } from '@sinclair/typebox'; 2 | import envSchema from 'env-schema'; 3 | 4 | const schema = Type.Object({ 5 | /** Hostname of the API server */ 6 | API_HOST: Type.String({ default: '0.0.0.0' }), 7 | /** Port in which to serve the API */ 8 | API_PORT: Type.Number({ default: 3000, minimum: 0, maximum: 65535 }), 9 | /** Port in which to serve the profiler */ 10 | PROFILER_PORT: Type.Number({ default: 9119 }), 11 | 12 | ORDINALS_PGHOST: Type.String(), 13 | ORDINALS_PGPORT: Type.Number({ default: 5432, minimum: 0, maximum: 65535 }), 14 | ORDINALS_PGUSER: Type.String(), 15 | ORDINALS_PGPASSWORD: Type.String(), 16 | ORDINALS_PGDATABASE: Type.String(), 17 | ORDINALS_SCHEMA: Type.Optional(Type.String()), 18 | 19 | BRC20_PGHOST: Type.String(), 20 | BRC20_PGPORT: Type.Number({ default: 5432, minimum: 0, maximum: 65535 }), 21 | BRC20_PGUSER: Type.String(), 22 | BRC20_PGPASSWORD: Type.String(), 23 | BRC20_PGDATABASE: Type.String(), 24 | BRC20_SCHEMA: Type.Optional(Type.String()), 25 | 26 | /** Limit to how many concurrent connections can be created */ 27 | PG_CONNECTION_POOL_MAX: Type.Number({ default: 10 }), 28 | PG_IDLE_TIMEOUT: Type.Number({ default: 30 }), 29 | PG_MAX_LIFETIME: Type.Number({ default: 60 }), 30 | PG_STATEMENT_TIMEOUT: Type.Number({ default: 60_000 }), 31 | }); 32 | type Env = Static; 33 | 34 | export const ENV = envSchema({ 35 | schema: schema, 36 | dotenv: true, 37 | }); 38 | -------------------------------------------------------------------------------- /api/ordinals/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildProfilerServer, 3 | isProdEnv, 4 | logger, 5 | registerShutdownConfig, 6 | } from '@hirosystems/api-toolkit'; 7 | import { buildApiServer, buildPromServer } from './api/init'; 8 | import { ENV } from './env'; 9 | import { ApiMetrics } from './metrics/metrics'; 10 | import { PgStore } from './pg/pg-store'; 11 | import { Brc20PgStore } from './pg/brc20/brc20-pg-store'; 12 | 13 | async function initApiService(db: PgStore, brc20Db: Brc20PgStore) { 14 | logger.info('Initializing API service...'); 15 | const fastify = await buildApiServer({ db, brc20Db }); 16 | registerShutdownConfig({ 17 | name: 'API Server', 18 | forceKillable: false, 19 | handler: async () => { 20 | await fastify.close(); 21 | }, 22 | }); 23 | 24 | await fastify.listen({ host: ENV.API_HOST, port: ENV.API_PORT }); 25 | 26 | if (isProdEnv) { 27 | const promServer = await buildPromServer({ metrics: fastify.metrics }); 28 | registerShutdownConfig({ 29 | name: 'Prometheus Server', 30 | forceKillable: false, 31 | handler: async () => { 32 | await promServer.close(); 33 | }, 34 | }); 35 | 36 | ApiMetrics.configure(db); 37 | await promServer.listen({ host: ENV.API_HOST, port: 9153 }); 38 | 39 | const profilerServer = await buildProfilerServer(); 40 | registerShutdownConfig({ 41 | name: 'Profiler Server', 42 | forceKillable: false, 43 | handler: async () => { 44 | await profilerServer.close(); 45 | }, 46 | }); 47 | await profilerServer.listen({ host: ENV.API_HOST, port: ENV.PROFILER_PORT }); 48 | } 49 | } 50 | 51 | async function initApp() { 52 | logger.info(`Initializing Ordinals API...`); 53 | const db = await PgStore.connect(); 54 | const brc20Db = await Brc20PgStore.connect(); 55 | await initApiService(db, brc20Db); 56 | registerShutdownConfig({ 57 | name: 'DB', 58 | forceKillable: false, 59 | handler: async () => { 60 | await db.close(); 61 | await brc20Db.close(); 62 | }, 63 | }); 64 | } 65 | 66 | registerShutdownConfig(); 67 | initApp() 68 | .then(() => { 69 | logger.info('App initialized'); 70 | }) 71 | .catch(error => { 72 | logger.error(error, `App failed to start`); 73 | process.exit(1); 74 | }); 75 | -------------------------------------------------------------------------------- /api/ordinals/src/metrics/metrics.ts: -------------------------------------------------------------------------------- 1 | import * as prom from 'prom-client'; 2 | import { PgStore } from '../pg/pg-store'; 3 | 4 | export class ApiMetrics { 5 | /** The most recent Bitcoin block height ingested by the API */ 6 | readonly ordinals_api_block_height: prom.Gauge; 7 | /** Maximum blessed inscription number */ 8 | readonly ordinals_api_max_inscription_number: prom.Gauge; 9 | /** Maximum cursed inscription number */ 10 | readonly ordinals_api_max_cursed_inscription_number: prom.Gauge; 11 | 12 | static configure(db: PgStore): ApiMetrics { 13 | return new ApiMetrics(db); 14 | } 15 | 16 | private constructor(db: PgStore) { 17 | this.ordinals_api_block_height = new prom.Gauge({ 18 | name: `ordinals_api_block_height`, 19 | help: 'The most recent Bitcoin block height ingested by the API', 20 | async collect() { 21 | const height = await db.getChainTipBlockHeight(); 22 | this.set(height ?? 0); 23 | }, 24 | }); 25 | this.ordinals_api_max_inscription_number = new prom.Gauge({ 26 | name: `ordinals_api_max_inscription_number`, 27 | help: 'Maximum blessed inscription number', 28 | async collect() { 29 | const max = await db.getMaxInscriptionNumber(); 30 | if (max) this.set(max); 31 | }, 32 | }); 33 | this.ordinals_api_max_cursed_inscription_number = new prom.Gauge({ 34 | name: `ordinals_api_max_cursed_inscription_number`, 35 | help: 'Maximum cursed inscription number', 36 | async collect() { 37 | const max = await db.getMaxCursedInscriptionNumber(); 38 | if (max) this.set(max); 39 | }, 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /api/ordinals/src/pg/brc20/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as postgres from 'postgres'; 2 | import { PgSqlClient } from '@hirosystems/api-toolkit'; 3 | 4 | export function sqlOr( 5 | sql: PgSqlClient, 6 | partials: postgres.PendingQuery[] | undefined 7 | ) { 8 | return partials?.reduce((acc, curr) => sql`${acc} OR ${curr}`); 9 | } 10 | -------------------------------------------------------------------------------- /api/ordinals/src/pg/brc20/types.ts: -------------------------------------------------------------------------------- 1 | export type DbBrc20Token = { 2 | ticker: string; 3 | display_ticker: string; 4 | inscription_id: string; 5 | inscription_number: string; 6 | block_height: string; 7 | block_hash: string; 8 | tx_id: string; 9 | tx_index: number; 10 | address: string; 11 | max: string; 12 | limit: string; 13 | decimals: number; 14 | self_mint: boolean; 15 | minted_supply: string; 16 | tx_count: string; 17 | timestamp: number; 18 | }; 19 | 20 | export type DbBrc20TokenWithSupply = DbBrc20Token & { 21 | minted_supply: string; 22 | holders: string; 23 | }; 24 | 25 | export type DbBrc20Holder = { 26 | address: string; 27 | total_balance: string; 28 | decimals: number; 29 | }; 30 | 31 | export type DbBrc20Balance = { 32 | ticker: string; 33 | decimals: number; 34 | avail_balance: string; 35 | trans_balance: string; 36 | total_balance: string; 37 | }; 38 | 39 | export enum DbBrc20EventOperation { 40 | deploy = 'deploy', 41 | mint = 'mint', 42 | transfer = 'transfer', 43 | transferSend = 'transfer_send', 44 | } 45 | 46 | export type DbBrc20Activity = { 47 | ticker: string; 48 | operation: DbBrc20EventOperation; 49 | inscription_id: string; 50 | inscription_number: string; 51 | ordinal_number: string; 52 | block_height: string; 53 | block_hash: string; 54 | tx_id: string; 55 | tx_index: number; 56 | output: string; 57 | offset: string; 58 | timestamp: number; 59 | amount: string; 60 | address: string; 61 | to_address: string | null; 62 | deploy_decimals: number; 63 | deploy_max: string; 64 | deploy_limit: string | null; 65 | }; 66 | 67 | export type DbBrc20TransferableInscription = { 68 | inscription_number: string; 69 | inscription_id: string; 70 | amount: string; 71 | ticker: string; 72 | ordinal_number: string; 73 | }; 74 | -------------------------------------------------------------------------------- /api/ordinals/src/pg/counts/helpers.ts: -------------------------------------------------------------------------------- 1 | import { objRemoveUndefinedValues } from '../helpers'; 2 | import { DbInscriptionIndexFilters } from '../types'; 3 | import { DbInscriptionIndexResultCountType } from './types'; 4 | 5 | /** 6 | * Returns which inscription count is required based on filters sent to the index endpoint. 7 | * @param filters - DbInscriptionIndexFilters 8 | * @returns DbInscriptionIndexResultCountType 9 | */ 10 | export function getIndexResultCountType( 11 | filters?: DbInscriptionIndexFilters 12 | ): DbInscriptionIndexResultCountType { 13 | if (!filters) return DbInscriptionIndexResultCountType.all; 14 | // How many filters do we have? 15 | objRemoveUndefinedValues(filters); 16 | switch (Object.keys(filters).length) { 17 | case 0: 18 | return DbInscriptionIndexResultCountType.all; 19 | case 1: 20 | if (filters.mime_type) return DbInscriptionIndexResultCountType.mimeType; 21 | if (filters.sat_rarity) return DbInscriptionIndexResultCountType.satRarity; 22 | if (filters.address) return DbInscriptionIndexResultCountType.address; 23 | if (filters.genesis_address) return DbInscriptionIndexResultCountType.genesisAddress; 24 | if (filters.genesis_block_height) return DbInscriptionIndexResultCountType.blockHeight; 25 | if (filters.from_genesis_block_height) 26 | return DbInscriptionIndexResultCountType.fromblockHeight; 27 | if (filters.to_genesis_block_height) return DbInscriptionIndexResultCountType.toblockHeight; 28 | if (filters.genesis_block_hash) return DbInscriptionIndexResultCountType.blockHash; 29 | if (filters.cursed !== undefined) return DbInscriptionIndexResultCountType.cursed; 30 | if (filters.recursive !== undefined) return DbInscriptionIndexResultCountType.recursive; 31 | if (filters.number || filters.genesis_id || filters.output || filters.sat_ordinal) 32 | return DbInscriptionIndexResultCountType.singleResult; 33 | case 2: 34 | if (filters.from_genesis_block_height && filters.to_genesis_block_height) 35 | return DbInscriptionIndexResultCountType.blockHeightRange; 36 | } 37 | return DbInscriptionIndexResultCountType.custom; 38 | } 39 | -------------------------------------------------------------------------------- /api/ordinals/src/pg/counts/types.ts: -------------------------------------------------------------------------------- 1 | /** Type of row count required for an inscription index endpoint call */ 2 | export enum DbInscriptionIndexResultCountType { 3 | /** All inscriptions */ 4 | all, 5 | /** Filtered by cursed or blessed */ 6 | cursed, 7 | /** Filtered by mime type */ 8 | mimeType, 9 | /** Filtered by sat rarity */ 10 | satRarity, 11 | /** Filtered by address */ 12 | address, 13 | genesisAddress, 14 | /** Filtered by block height */ 15 | blockHeight, 16 | fromblockHeight, 17 | toblockHeight, 18 | blockHeightRange, 19 | /** Filtered by block hash */ 20 | blockHash, 21 | /** Filtered by recursive */ 22 | recursive, 23 | /** Filtered by some other param that yields a single result (easy to count) */ 24 | singleResult, 25 | /** Filtered by custom arguments (tough to count) */ 26 | custom, 27 | } 28 | -------------------------------------------------------------------------------- /api/ordinals/src/pg/helpers.ts: -------------------------------------------------------------------------------- 1 | export function objRemoveUndefinedValues(obj: object) { 2 | Object.keys(obj).forEach(key => (obj as any)[key] === undefined && delete (obj as any)[key]); 3 | } 4 | -------------------------------------------------------------------------------- /api/ordinals/src/pg/types.ts: -------------------------------------------------------------------------------- 1 | import { Order, OrderBy } from '../api/schemas'; 2 | import { SatoshiRarity } from '../api/util/ordinal-satoshi'; 3 | 4 | export type DbPaginatedResult = { 5 | total: number; 6 | results: T[]; 7 | }; 8 | 9 | export type DbFullyLocatedInscriptionResult = { 10 | genesis_id: string; 11 | genesis_block_height: string; 12 | genesis_block_hash: string; 13 | genesis_tx_id: string; 14 | genesis_fee: bigint; 15 | genesis_timestamp: number; 16 | genesis_address: string; 17 | number: string; 18 | address: string | null; 19 | tx_id: string; 20 | tx_index: number; 21 | output: string; 22 | offset: string | null; 23 | value: string | null; 24 | sat_ordinal: string; 25 | sat_rarity: string; 26 | sat_coinbase_height: string; 27 | mime_type: string; 28 | content_type: string; 29 | content_length: string; 30 | timestamp: number; 31 | curse_type: string | null; 32 | recursive: boolean; 33 | recursion_refs: string | null; 34 | parent_refs: string | null; 35 | metadata: string | null; 36 | input_index: number; 37 | pointer: number | null; 38 | metaprotocol: string | null; 39 | delegate: string | null; 40 | charms: string; 41 | }; 42 | 43 | export type DbLocation = { 44 | genesis_id: string; 45 | block_height: string; 46 | block_hash: string; 47 | tx_id: string; 48 | tx_index: number; 49 | address: string; 50 | output: string; 51 | offset: string | null; 52 | prev_output: string | null; 53 | prev_offset: string | null; 54 | value: string | null; 55 | timestamp: number; 56 | }; 57 | 58 | export type DbInscriptionLocationChange = { 59 | genesis_id: string; 60 | number: string; 61 | from_block_height: string; 62 | from_block_hash: string; 63 | from_tx_id: string; 64 | from_address: string; 65 | from_output: string; 66 | from_offset: string | null; 67 | from_value: string | null; 68 | from_timestamp: number; 69 | to_block_height: string; 70 | to_block_hash: string; 71 | to_tx_id: string; 72 | to_address: string; 73 | to_output: string; 74 | to_offset: string | null; 75 | to_value: string | null; 76 | to_timestamp: number; 77 | }; 78 | 79 | export type DbInscriptionContent = { 80 | content_type: string; 81 | content_length: string; 82 | content: string; 83 | }; 84 | 85 | export type DbInscriptionIndexPaging = { 86 | limit: number; 87 | offset: number; 88 | }; 89 | 90 | export type DbInscriptionIndexFilters = { 91 | genesis_id?: string[]; 92 | genesis_block_height?: number; 93 | from_genesis_block_height?: number; 94 | to_genesis_block_height?: number; 95 | genesis_block_hash?: string; 96 | from_genesis_timestamp?: number; 97 | to_genesis_timestamp?: number; 98 | from_sat_coinbase_height?: number; 99 | to_sat_coinbase_height?: number; 100 | number?: number[]; 101 | from_number?: number; 102 | to_number?: number; 103 | address?: string[]; 104 | genesis_address?: string[]; 105 | mime_type?: string[]; 106 | output?: string; 107 | sat_rarity?: SatoshiRarity[]; 108 | sat_ordinal?: bigint; 109 | from_sat_ordinal?: bigint; 110 | to_sat_ordinal?: bigint; 111 | recursive?: boolean; 112 | cursed?: boolean; 113 | }; 114 | 115 | export type DbInscriptionIndexOrder = { 116 | order_by?: OrderBy; 117 | order?: Order; 118 | }; 119 | 120 | export enum DbInscriptionType { 121 | blessed = 'blessed', 122 | cursed = 'cursed', 123 | } 124 | 125 | export type DbInscriptionCountPerBlockFilters = { 126 | from_block_height?: number; 127 | to_block_height?: number; 128 | }; 129 | 130 | export type DbInscriptionCountPerBlock = { 131 | block_height: string; 132 | block_hash: string; 133 | inscription_count: string; 134 | inscription_count_accum: string; 135 | timestamp: number; 136 | }; 137 | -------------------------------------------------------------------------------- /api/ordinals/tests/api/ordinal-satoshi.test.ts: -------------------------------------------------------------------------------- 1 | import { OrdinalSatoshi, SatoshiRarity } from '../../src/api/util/ordinal-satoshi'; 2 | 3 | describe('OrdinalSatoshi', () => { 4 | test('mythic sat', () => { 5 | const sat = new OrdinalSatoshi(0); 6 | expect(sat.rarity).toBe(SatoshiRarity.mythic); 7 | expect(sat.degree).toBe('0°0′0″0‴'); 8 | expect(sat.decimal).toBe('0.0'); 9 | expect(sat.cycle).toBe(0); 10 | expect(sat.epoch).toBe(0); 11 | expect(sat.name).toBe('nvtdijuwxlp'); 12 | expect(sat.offset).toBe(0); 13 | expect(sat.percentile).toBe('0%'); 14 | expect(sat.period).toBe(0); 15 | expect(sat.blockHeight).toBe(0); 16 | }); 17 | 18 | test('legendary sat', () => { 19 | const sat = new OrdinalSatoshi(2067187500000000); 20 | expect(sat.rarity).toBe(SatoshiRarity.legendary); 21 | expect(sat.degree).toBe('1°0′0″0‴'); 22 | expect(sat.decimal).toBe('1260000.0'); 23 | expect(sat.cycle).toBe(1); 24 | expect(sat.epoch).toBe(6); 25 | expect(sat.name).toBe('fachfvytgb'); 26 | expect(sat.offset).toBe(0); 27 | expect(sat.percentile).toBe('98.4375001082813%'); 28 | expect(sat.period).toBe(625); 29 | expect(sat.blockHeight).toBe(1260000); 30 | }); 31 | 32 | test('epic sat', () => { 33 | const sat = new OrdinalSatoshi(1050000000000000); 34 | expect(sat.rarity).toBe(SatoshiRarity.epic); 35 | expect(sat.degree).toBe('0°0′336″0‴'); 36 | expect(sat.decimal).toBe('210000.0'); 37 | expect(sat.cycle).toBe(0); 38 | expect(sat.epoch).toBe(1); 39 | expect(sat.name).toBe('gkjbdrhkfqf'); 40 | expect(sat.offset).toBe(0); 41 | expect(sat.percentile).toBe('50.00000005500003%'); 42 | expect(sat.period).toBe(104); 43 | expect(sat.blockHeight).toBe(210000); 44 | }); 45 | 46 | test('rare sat', () => { 47 | const sat = new OrdinalSatoshi(10080000000000); 48 | expect(sat.rarity).toBe(SatoshiRarity.rare); 49 | expect(sat.degree).toBe('0°2016′0″0‴'); 50 | expect(sat.decimal).toBe('2016.0'); 51 | expect(sat.cycle).toBe(0); 52 | expect(sat.epoch).toBe(0); 53 | expect(sat.name).toBe('ntwwidfrzxh'); 54 | expect(sat.offset).toBe(0); 55 | expect(sat.percentile).toBe('0.48000000052800024%'); 56 | expect(sat.period).toBe(1); 57 | expect(sat.blockHeight).toBe(2016); 58 | }); 59 | 60 | test('uncommon sat', () => { 61 | const sat = new OrdinalSatoshi(5000000000); 62 | expect(sat.rarity).toBe(SatoshiRarity.uncommon); 63 | expect(sat.degree).toBe('0°1′1″0‴'); 64 | expect(sat.decimal).toBe('1.0'); 65 | expect(sat.cycle).toBe(0); 66 | expect(sat.epoch).toBe(0); 67 | expect(sat.name).toBe('nvtcsezkbth'); 68 | expect(sat.offset).toBe(0); 69 | expect(sat.percentile).toBe('0.00023809523835714296%'); 70 | expect(sat.period).toBe(0); 71 | expect(sat.blockHeight).toBe(1); 72 | }); 73 | 74 | test('common sat', () => { 75 | const sat = new OrdinalSatoshi(200); 76 | expect(sat.rarity).toBe(SatoshiRarity.common); 77 | expect(sat.degree).toBe('0°0′0″200‴'); 78 | expect(sat.decimal).toBe('0.200'); 79 | expect(sat.cycle).toBe(0); 80 | expect(sat.epoch).toBe(0); 81 | expect(sat.name).toBe('nvtdijuwxdx'); 82 | expect(sat.offset).toBe(200); 83 | expect(sat.percentile).toBe('0.000000000009523809534285719%'); 84 | expect(sat.period).toBe(0); 85 | expect(sat.blockHeight).toBe(0); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /api/ordinals/tests/api/status.test.ts: -------------------------------------------------------------------------------- 1 | import { buildApiServer } from '../../src/api/init'; 2 | import { Brc20PgStore } from '../../src/pg/brc20/brc20-pg-store'; 3 | import { PgStore } from '../../src/pg/pg-store'; 4 | import { 5 | TestFastifyServer, 6 | ORDINALS_MIGRATIONS_DIR, 7 | BRC20_MIGRATIONS_DIR, 8 | clearDb, 9 | runMigrations, 10 | inscriptionReveal, 11 | updateTestChainTip, 12 | } from '../helpers'; 13 | 14 | describe('Status', () => { 15 | let db: PgStore; 16 | let brc20Db: Brc20PgStore; 17 | let fastify: TestFastifyServer; 18 | 19 | beforeEach(async () => { 20 | db = await PgStore.connect(); 21 | await runMigrations(db.sql, ORDINALS_MIGRATIONS_DIR); 22 | brc20Db = await Brc20PgStore.connect(); 23 | await runMigrations(brc20Db.sql, BRC20_MIGRATIONS_DIR); 24 | fastify = await buildApiServer({ db, brc20Db }); 25 | }); 26 | 27 | afterEach(async () => { 28 | await fastify.close(); 29 | await clearDb(db.sql); 30 | await db.close(); 31 | await clearDb(brc20Db.sql); 32 | await brc20Db.close(); 33 | }); 34 | 35 | test('returns status when db is empty', async () => { 36 | const response = await fastify.inject({ method: 'GET', url: '/ordinals/v1/' }); 37 | const json = response.json(); 38 | expect(json).toStrictEqual({ 39 | server_version: 'bitcoin-indexer-ordinals-api v0.0.1 (test:123456)', 40 | status: 'ready', 41 | }); 42 | const noVersionResponse = await fastify.inject({ method: 'GET', url: '/ordinals/' }); 43 | expect(response.statusCode).toEqual(noVersionResponse.statusCode); 44 | expect(json).toStrictEqual(noVersionResponse.json()); 45 | }); 46 | 47 | test('returns inscriptions total', async () => { 48 | await inscriptionReveal(db.sql, { 49 | inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', 50 | ordinal_number: '257418248345364', 51 | number: '0', 52 | classic_number: '0', 53 | block_height: '775617', 54 | block_hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', 55 | tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', 56 | tx_index: 0, 57 | address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', 58 | mime_type: 'text/plain', 59 | content_type: 'text/plain;charset=utf-8', 60 | content_length: 5, 61 | content: '0x48656C6C6F', 62 | fee: '2805', 63 | curse_type: null, 64 | recursive: false, 65 | input_index: 0, 66 | pointer: null, 67 | metadata: null, 68 | metaprotocol: null, 69 | delegate: null, 70 | timestamp: 1676913207000, 71 | output: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0', 72 | offset: '0', 73 | prev_output: null, 74 | prev_offset: null, 75 | value: '10000', 76 | transfer_type: 'transferred', 77 | rarity: 'common', 78 | coinbase_height: '650000', 79 | charms: 0, 80 | }); 81 | await inscriptionReveal(db.sql, { 82 | inscription_id: 'a98d7055a77fa0b96cc31e30bb8bacf777382d1b67f1b7eca6f2014e961591c8i0', 83 | ordinal_number: '257418248345364', 84 | number: '-2', 85 | classic_number: '-2', 86 | block_height: '791975', 87 | block_hash: '6c3f7e89a7b6d5f4e3a2c1b09876e5d4c3b2a1908765e4d3c2b1a09f8e7d6c5b', 88 | tx_id: 'a98d7055a77fa0b96cc31e30bb8bacf777382d1b67f1b7eca6f2014e961591c8', 89 | tx_index: 0, 90 | address: 'bc1pk6y72s45lcaurfwxrjyg7cf9xa9ezzuc8f5hhhzhtvhe5fgygckq0t0m5f', 91 | mime_type: 'text/plain', 92 | content_type: 'text/plain;charset=utf-8', 93 | content_length: 5, 94 | content: '0x48656C6C6F', 95 | fee: '2805', 96 | curse_type: 'p2wsh', 97 | recursive: false, 98 | input_index: 0, 99 | pointer: null, 100 | metadata: null, 101 | metaprotocol: null, 102 | delegate: null, 103 | timestamp: 1676913207000, 104 | output: 'a98d7055a77fa0b96cc31e30bb8bacf777382d1b67f1b7eca6f2014e961591c8:0', 105 | offset: '0', 106 | prev_output: null, 107 | prev_offset: null, 108 | value: '10000', 109 | transfer_type: 'transferred', 110 | rarity: 'common', 111 | coinbase_height: '650000', 112 | charms: 0, 113 | }); 114 | await updateTestChainTip(db.sql, 791975); 115 | 116 | const response = await fastify.inject({ method: 'GET', url: '/ordinals/v1/' }); 117 | const json = response.json(); 118 | expect(json).toStrictEqual({ 119 | server_version: 'bitcoin-indexer-ordinals-api v0.0.1 (test:123456)', 120 | status: 'ready', 121 | block_height: 791975, 122 | max_inscription_number: 0, 123 | max_cursed_inscription_number: -2, 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /api/ordinals/tests/setup.ts: -------------------------------------------------------------------------------- 1 | // ts-unused-exports:disable-next-line 2 | export default (): void => { 3 | process.env.API_HOST = '0.0.0.0'; 4 | process.env.API_PORT = '3000'; 5 | process.env.ORDINALS_PGHOST = '127.0.0.1'; 6 | process.env.ORDINALS_PGPORT = '5432'; 7 | process.env.ORDINALS_PGUSER = 'postgres'; 8 | process.env.ORDINALS_PGPASSWORD = 'postgres'; 9 | process.env.ORDINALS_PGDATABASE = 'postgres'; 10 | process.env.ORDINALS_SCHEMA = 'public'; 11 | process.env.BRC20_PGHOST = '127.0.0.1'; 12 | process.env.BRC20_PGPORT = '5432'; 13 | process.env.BRC20_PGUSER = 'postgres'; 14 | process.env.BRC20_PGPASSWORD = 'postgres'; 15 | process.env.BRC20_PGDATABASE = 'postgres'; 16 | process.env.BRC20_SCHEMA = 'public'; 17 | }; 18 | -------------------------------------------------------------------------------- /api/ordinals/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "tests/**/*.ts", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /api/ordinals/util/openapi-generator.ts: -------------------------------------------------------------------------------- 1 | import Fastify, { FastifyPluginAsync } from 'fastify'; 2 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 3 | import { Api } from '../src/api/init'; 4 | import FastifySwagger from '@fastify/swagger'; 5 | import { existsSync, mkdirSync, writeFileSync } from 'fs'; 6 | import { Server } from 'http'; 7 | import { OpenApiSchemaOptions } from '../src/api/schemas'; 8 | 9 | /** 10 | * Generates `openapi.yaml` based on current Swagger definitions. 11 | */ 12 | export const ApiGenerator: FastifyPluginAsync< 13 | Record, 14 | Server, 15 | TypeBoxTypeProvider 16 | > = async (fastify, options) => { 17 | await fastify.register(FastifySwagger, OpenApiSchemaOptions); 18 | await fastify.register(Api, { prefix: '/ordinals/v1' }); 19 | if (!existsSync('./tmp')) { 20 | mkdirSync('./tmp'); 21 | } 22 | writeFileSync('./tmp/openapi.yaml', fastify.swagger({ yaml: true })); 23 | writeFileSync('./tmp/openapi.json', JSON.stringify(fastify.swagger(), null, 2)); 24 | }; 25 | 26 | const fastify = Fastify({ 27 | trustProxy: true, 28 | logger: true, 29 | }).withTypeProvider(); 30 | 31 | void fastify.register(ApiGenerator).then(async () => { 32 | await fastify.close(); 33 | }); 34 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcoin-indexer", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Hiro Systems PBC (https://hiro.so)", 6 | "license": "Apache 2.0", 7 | "scripts": { 8 | "generate:vercel:ordinals": "npm i --prefix ordinals && npm run generate:vercel --prefix ordinals && mkdir -p ./tmp/ordinals && mv ordinals/tmp/* ./tmp/ordinals", 9 | "generate:vercel:runes": "npm i --prefix runes && npm run generate:vercel --prefix runes && mkdir -p ./tmp/runes && mv runes/tmp/* ./tmp/runes", 10 | "generate:vercel": "rimraf ./tmp && npm run generate:vercel:ordinals && npm run generate:vercel:runes" 11 | }, 12 | "devDependencies": { 13 | "rimraf": "^6.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/runes/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .eslintrc.js 3 | -------------------------------------------------------------------------------- /api/runes/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@stacks/eslint-config', 'prettier'], 4 | overrides: [], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | tsconfigRootDir: __dirname, 8 | project: './tsconfig.json', 9 | ecmaVersion: 2020, 10 | sourceType: 'module', 11 | }, 12 | ignorePatterns: ['*.config.js', 'config/*', '*.mjs', 'tests/*.js', 'client/*'], 13 | plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc', 'prettier'], 14 | rules: { 15 | 'prettier/prettier': 'error', 16 | '@typescript-eslint/no-inferrable-types': 'off', 17 | '@typescript-eslint/camelcase': 'off', 18 | '@typescript-eslint/no-empty-function': 'off', 19 | '@typescript-eslint/no-use-before-define': ['error', 'nofunc'], 20 | '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], 21 | 'no-warning-comments': 'warn', 22 | 'tsdoc/syntax': 'error', 23 | // TODO: Remove this when `any` abi type is fixed. 24 | '@typescript-eslint/no-unsafe-assignment': 'off', 25 | '@typescript-eslint/no-unsafe-member-access': 'off', 26 | '@typescript-eslint/no-unsafe-call': 'off', 27 | '@typescript-eslint/restrict-template-expressions': 'off', 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /api/runes/.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /api/runes/README.md: -------------------------------------------------------------------------------- 1 | # Runes API 2 | -------------------------------------------------------------------------------- /api/runes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runes-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rimraf ./dist && tsc --project tsconfig.build.json", 8 | "start": "node dist/src/index.js", 9 | "start-ts": "ts-node ./src/index.ts", 10 | "test": "jest --runInBand", 11 | "generate:openapi": "rimraf ./tmp && node -r ts-node/register ./util/openapi-generator.ts", 12 | "generate:docs": "redoc-cli build --output ./tmp/index.html ./tmp/openapi.yaml", 13 | "generate:git-info": "rimraf .git-info && node_modules/.bin/api-toolkit-git-info", 14 | "generate:vercel": "npm run generate:git-info && npm run generate:openapi && npm run generate:docs", 15 | "lint:eslint": "eslint . --ext .ts,.tsx -f unix", 16 | "lint:prettier": "prettier --check src/**/*.ts tests/**/*.ts", 17 | "lint:unused-exports": "ts-unused-exports tsconfig.json --showLineNumber --excludePathsFromReport=util/*", 18 | "testenv:run": "docker compose -f ../../dockerfiles/docker-compose.dev.postgres.yml up", 19 | "testenv:stop": "docker compose -f ../../dockerfiles/docker-compose.dev.postgres.yml down -v -t 0", 20 | "testenv:logs": "docker compose -f ../../dockerfiles/docker-compose.dev.postgres.yml logs -t -f" 21 | }, 22 | "author": "Hiro Systems PBC (https://hiro.so)", 23 | "license": "Apache 2.0", 24 | "prettier": "@stacks/prettier-config", 25 | "devDependencies": { 26 | "@commitlint/cli": "^17.4.3", 27 | "@commitlint/config-conventional": "^17.4.3", 28 | "@semantic-release/changelog": "^6.0.3", 29 | "@semantic-release/commit-analyzer": "^10.0.4", 30 | "@semantic-release/git": "^10.0.1", 31 | "@stacks/eslint-config": "^1.2.0", 32 | "@types/jest": "^29.2.4", 33 | "@types/supertest": "^2.0.12", 34 | "@typescript-eslint/eslint-plugin": "^5.46.1", 35 | "@typescript-eslint/parser": "^5.51.0", 36 | "babel-jest": "^29.3.1", 37 | "conventional-changelog-conventionalcommits": "^6.1.0", 38 | "eslint": "^8.29.0", 39 | "eslint-plugin-prettier": "^4.2.1", 40 | "eslint-plugin-tsdoc": "^0.2.17", 41 | "husky": "^8.0.3", 42 | "jest": "^29.3.1", 43 | "prettier": "^2.8.1", 44 | "redoc-cli": "^0.13.20", 45 | "rimraf": "^3.0.2", 46 | "ts-jest": "^29.0.3", 47 | "ts-node": "^10.8.2", 48 | "ts-unused-exports": "^10.0.1", 49 | "typescript": "^4.7.4" 50 | }, 51 | "dependencies": { 52 | "@fastify/cors": "^8.0.0", 53 | "@fastify/formbody": "^7.0.1", 54 | "@fastify/multipart": "^7.1.0", 55 | "@fastify/swagger": "^8.3.1", 56 | "@fastify/type-provider-typebox": "3.2.0", 57 | "@hirosystems/api-toolkit": "^1.6.0", 58 | "@types/node": "^18.13.0", 59 | "bignumber.js": "^9.1.2", 60 | "env-schema": "^5.2.1", 61 | "fastify": "4.15.0", 62 | "fastify-metrics": "10.2.0", 63 | "pino": "^8.10.0", 64 | "postgres": "^3.3.4" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /api/runes/src/@types/fastify/index.d.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify'; 2 | import { PgStore } from '../../pg/pg-store'; 3 | 4 | declare module 'fastify' { 5 | export interface FastifyInstance< 6 | HttpServer = Server, 7 | HttpRequest = IncomingMessage, 8 | HttpResponse = ServerResponse, 9 | Logger = FastifyLoggerInstance, 10 | TypeProvider = FastifyTypeProviderDefault 11 | > { 12 | db: PgStore; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/runes/src/api/init.ts: -------------------------------------------------------------------------------- 1 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 2 | import FastifyCors from '@fastify/cors'; 3 | import Fastify, { FastifyInstance } from 'fastify'; 4 | import FastifyMetrics, { IFastifyMetrics } from 'fastify-metrics'; 5 | import { FastifyPluginAsync } from 'fastify'; 6 | import { Server } from 'http'; 7 | import { PgStore } from '../pg/pg-store'; 8 | import { EtchingRoutes } from './routes/etchings'; 9 | import { AddressRoutes } from './routes/addresses'; 10 | import { TransactionRoutes } from './routes/transactions'; 11 | import { BlockRoutes } from './routes/blocks'; 12 | import { StatusRoutes } from './routes/status'; 13 | import { PINO_LOGGER_CONFIG, isProdEnv } from '@hirosystems/api-toolkit'; 14 | 15 | export const Api: FastifyPluginAsync< 16 | Record, 17 | Server, 18 | TypeBoxTypeProvider 19 | > = async fastify => { 20 | await fastify.register(StatusRoutes); 21 | await fastify.register(EtchingRoutes); 22 | await fastify.register(AddressRoutes); 23 | await fastify.register(TransactionRoutes); 24 | await fastify.register(BlockRoutes); 25 | }; 26 | 27 | export async function buildApiServer(args: { db: PgStore }) { 28 | const fastify = Fastify({ 29 | trustProxy: true, 30 | logger: PINO_LOGGER_CONFIG, 31 | }).withTypeProvider(); 32 | if (isProdEnv) { 33 | await fastify.register(FastifyMetrics, { endpoint: null }); 34 | } 35 | await fastify.register(FastifyCors); 36 | fastify.decorate('db', args.db); 37 | await fastify.register(Api, { prefix: '/runes/v1' }); 38 | await fastify.register(Api, { prefix: '/runes' }); 39 | 40 | return fastify; 41 | } 42 | 43 | export async function buildPrometheusServer(args: { 44 | metrics: IFastifyMetrics; 45 | }): Promise { 46 | const promServer = Fastify({ 47 | trustProxy: true, 48 | logger: PINO_LOGGER_CONFIG, 49 | }); 50 | promServer.route({ 51 | url: '/metrics', 52 | method: 'GET', 53 | logLevel: 'info', 54 | handler: async (_, reply) => { 55 | await reply.type('text/plain').send(await args.metrics.client.register.metrics()); 56 | }, 57 | }); 58 | return promServer; 59 | } 60 | -------------------------------------------------------------------------------- /api/runes/src/api/routes/addresses.ts: -------------------------------------------------------------------------------- 1 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 2 | import { Type } from '@sinclair/typebox'; 3 | import { FastifyPluginCallback } from 'fastify'; 4 | import { Server } from 'http'; 5 | import { 6 | AddressSchema, 7 | LimitSchema, 8 | OffsetSchema, 9 | BalanceResponseSchema, 10 | ActivityResponseSchema, 11 | } from '../schemas'; 12 | import { parseActivityResponse, parseBalanceResponse } from '../util/helpers'; 13 | import { Optional, PaginatedResponse } from '@hirosystems/api-toolkit'; 14 | import { handleCache } from '../util/cache'; 15 | 16 | export const AddressRoutes: FastifyPluginCallback< 17 | Record, 18 | Server, 19 | TypeBoxTypeProvider 20 | > = (fastify, options, done) => { 21 | fastify.addHook('preHandler', handleCache); 22 | 23 | fastify.get( 24 | '/addresses/:address/balances', 25 | { 26 | schema: { 27 | operationId: 'getAddressBalances', 28 | summary: 'Address balances', 29 | description: 'Retrieves a paginated list of address balances', 30 | tags: ['Balances'], 31 | params: Type.Object({ 32 | address: AddressSchema, 33 | }), 34 | querystring: Type.Object({ 35 | offset: Optional(OffsetSchema), 36 | limit: Optional(LimitSchema), 37 | }), 38 | response: { 39 | 200: PaginatedResponse(BalanceResponseSchema, 'Paginated balances response'), 40 | }, 41 | }, 42 | }, 43 | async (request, reply) => { 44 | const offset = request.query.offset ?? 0; 45 | const limit = request.query.limit ?? 20; 46 | const results = await fastify.db.getAddressBalances(request.params.address, offset, limit); 47 | await reply.send({ 48 | limit, 49 | offset, 50 | total: results.total, 51 | results: results.results.map(r => parseBalanceResponse(r)), 52 | }); 53 | } 54 | ); 55 | 56 | fastify.get( 57 | '/addresses/:address/activity', 58 | { 59 | schema: { 60 | operationId: 'getAddressActivity', 61 | summary: 'Address activity', 62 | description: 'Retrieves a paginated list of rune activity for an address', 63 | tags: ['Activity'], 64 | params: Type.Object({ 65 | address: AddressSchema, 66 | }), 67 | querystring: Type.Object({ 68 | offset: Optional(OffsetSchema), 69 | limit: Optional(LimitSchema), 70 | }), 71 | response: { 72 | 200: PaginatedResponse(ActivityResponseSchema, 'Paginated activity response'), 73 | }, 74 | }, 75 | }, 76 | async (request, reply) => { 77 | const offset = request.query.offset ?? 0; 78 | const limit = request.query.limit ?? 20; 79 | const results = await fastify.db.getAddressActivity(request.params.address, offset, limit); 80 | await reply.send({ 81 | limit, 82 | offset, 83 | total: results.total, 84 | results: results.results.map(r => parseActivityResponse(r)), 85 | }); 86 | } 87 | ); 88 | 89 | done(); 90 | }; 91 | -------------------------------------------------------------------------------- /api/runes/src/api/routes/blocks.ts: -------------------------------------------------------------------------------- 1 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 2 | import { Type } from '@sinclair/typebox'; 3 | import { FastifyPluginCallback } from 'fastify'; 4 | import { Server } from 'http'; 5 | import { LimitSchema, OffsetSchema, ActivityResponseSchema, BlockSchema } from '../schemas'; 6 | import { parseActivityResponse } from '../util/helpers'; 7 | import { Optional, PaginatedResponse } from '@hirosystems/api-toolkit'; 8 | import { handleCache } from '../util/cache'; 9 | 10 | export const BlockRoutes: FastifyPluginCallback< 11 | Record, 12 | Server, 13 | TypeBoxTypeProvider 14 | > = (fastify, options, done) => { 15 | fastify.addHook('preHandler', handleCache); 16 | 17 | fastify.get( 18 | '/blocks/:block/activity', 19 | { 20 | schema: { 21 | operationId: 'getBlockActivity', 22 | summary: 'Block activity', 23 | description: 'Retrieves a paginated list of rune activity for a block', 24 | tags: ['Activity'], 25 | params: Type.Object({ 26 | block: BlockSchema, 27 | }), 28 | querystring: Type.Object({ 29 | offset: Optional(OffsetSchema), 30 | limit: Optional(LimitSchema), 31 | operation_type: Optional( 32 | Type.Union( 33 | [ 34 | Type.Literal('etching'), 35 | Type.Literal('mint'), 36 | Type.Literal('burn'), 37 | Type.Literal('send'), 38 | Type.Literal('receive'), 39 | ], 40 | { 41 | title: 'OperationType', 42 | description: 'Filter activities by operation type', 43 | } 44 | ) 45 | ), 46 | }), 47 | response: { 48 | 200: PaginatedResponse(ActivityResponseSchema, 'Paginated activity response'), 49 | }, 50 | }, 51 | }, 52 | async (request, reply) => { 53 | const offset = request.query.offset ?? 0; 54 | const limit = request.query.limit ?? 20; 55 | const operationType = request.query.operation_type; 56 | 57 | const results = await fastify.db.getBlockActivity( 58 | request.params.block, 59 | offset, 60 | limit, 61 | operationType 62 | ); 63 | await reply.send({ 64 | limit, 65 | offset, 66 | total: results.total, 67 | results: results.results.map(r => parseActivityResponse(r)), 68 | }); 69 | } 70 | ); 71 | 72 | done(); 73 | }; 74 | -------------------------------------------------------------------------------- /api/runes/src/api/routes/status.ts: -------------------------------------------------------------------------------- 1 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 2 | import { FastifyPluginCallback } from 'fastify'; 3 | import { Server } from 'http'; 4 | import { ApiStatusResponse } from '../schemas'; 5 | import { SERVER_VERSION } from '@hirosystems/api-toolkit'; 6 | import { handleCache } from '../util/cache'; 7 | 8 | export const StatusRoutes: FastifyPluginCallback< 9 | Record, 10 | Server, 11 | TypeBoxTypeProvider 12 | > = (fastify, options, done) => { 13 | fastify.addHook('preHandler', handleCache); 14 | 15 | fastify.get( 16 | '/', 17 | { 18 | schema: { 19 | operationId: 'getApiStatus', 20 | summary: 'API Status', 21 | description: 'Displays the status of the API', 22 | tags: ['Status'], 23 | response: { 24 | 200: ApiStatusResponse, 25 | }, 26 | }, 27 | }, 28 | async (request, reply) => { 29 | const result = await fastify.db.sqlTransaction(async sql => { 30 | const block_height = await fastify.db.getChainTipBlockHeight(); 31 | return { 32 | server_version: `bitcoin-indexer-runes-api ${SERVER_VERSION.tag} (${SERVER_VERSION.branch}:${SERVER_VERSION.commit})`, 33 | status: 'ready', 34 | block_height: block_height ? parseInt(block_height) : undefined, 35 | }; 36 | }); 37 | await reply.send(result); 38 | } 39 | ); 40 | 41 | done(); 42 | }; 43 | -------------------------------------------------------------------------------- /api/runes/src/api/routes/transactions.ts: -------------------------------------------------------------------------------- 1 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 2 | import { Type } from '@sinclair/typebox'; 3 | import { FastifyPluginCallback } from 'fastify'; 4 | import { Server } from 'http'; 5 | import { LimitSchema, OffsetSchema, ActivityResponseSchema, TransactionIdSchema } from '../schemas'; 6 | import { parseActivityResponse } from '../util/helpers'; 7 | import { Optional, PaginatedResponse } from '@hirosystems/api-toolkit'; 8 | import { handleCache } from '../util/cache'; 9 | 10 | export const TransactionRoutes: FastifyPluginCallback< 11 | Record, 12 | Server, 13 | TypeBoxTypeProvider 14 | > = (fastify, options, done) => { 15 | fastify.addHook('preHandler', handleCache); 16 | 17 | fastify.get( 18 | '/transactions/:tx_id/activity', 19 | { 20 | schema: { 21 | operationId: 'getTransactionActivity', 22 | summary: 'Transaction activity', 23 | description: 'Retrieves a paginated list of rune activity for a transaction', 24 | tags: ['Activity'], 25 | params: Type.Object({ 26 | tx_id: TransactionIdSchema, 27 | }), 28 | querystring: Type.Object({ 29 | offset: Optional(OffsetSchema), 30 | limit: Optional(LimitSchema), 31 | }), 32 | response: { 33 | 200: PaginatedResponse(ActivityResponseSchema, 'Paginated activity response'), 34 | }, 35 | }, 36 | }, 37 | async (request, reply) => { 38 | const offset = request.query.offset ?? 0; 39 | const limit = request.query.limit ?? 20; 40 | const results = await fastify.db.getTransactionActivity(request.params.tx_id, offset, limit); 41 | await reply.send({ 42 | limit, 43 | offset, 44 | total: results.total, 45 | results: results.results.map(r => parseActivityResponse(r)), 46 | }); 47 | } 48 | ); 49 | 50 | done(); 51 | }; 52 | -------------------------------------------------------------------------------- /api/runes/src/api/util/cache.ts: -------------------------------------------------------------------------------- 1 | import { CACHE_CONTROL_MUST_REVALIDATE, parseIfNoneMatchHeader } from '@hirosystems/api-toolkit'; 2 | import { FastifyReply, FastifyRequest } from 'fastify'; 3 | 4 | export async function handleCache(request: FastifyRequest, reply: FastifyReply) { 5 | const ifNoneMatch = parseIfNoneMatchHeader(request.headers['if-none-match']); 6 | const etag = await request.server.db.getChainTipEtag(); 7 | if (etag) { 8 | if (ifNoneMatch && ifNoneMatch.includes(etag)) { 9 | await reply.header('Cache-Control', CACHE_CONTROL_MUST_REVALIDATE).code(304).send(); 10 | } else { 11 | void reply.headers({ 'Cache-Control': CACHE_CONTROL_MUST_REVALIDATE, ETag: `"${etag}"` }); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/runes/src/api/util/helpers.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { DbBalance, DbItemWithRune, DbLedgerEntry, DbRuneWithChainTip } from '../../pg/types'; 3 | import { EtchingResponse, ActivityResponse, BalanceResponse } from '../schemas'; 4 | 5 | function divisibility(num: string | BigNumber, decimals: number): string { 6 | return new BigNumber(num).shiftedBy(-1 * decimals).toFixed(decimals); 7 | } 8 | 9 | export function parseEtchingResponse(rune: DbRuneWithChainTip): EtchingResponse { 10 | let mintable = true; 11 | const minted = rune.minted == null ? '0' : rune.minted; 12 | const total_mints = rune.total_mints == null ? '0' : rune.total_mints; 13 | const burned = rune.burned == null ? '0' : rune.burned; 14 | const total_burns = rune.total_burns == null ? '0' : rune.total_burns; 15 | if ( 16 | rune.terms_amount == null || 17 | rune.cenotaph || 18 | (rune.terms_cap && BigNumber(total_mints).gte(rune.terms_cap)) || 19 | (rune.terms_height_start && BigNumber(rune.chain_tip).lt(rune.terms_height_start)) || 20 | (rune.terms_height_end && BigNumber(rune.chain_tip).gt(rune.terms_height_end)) || 21 | (rune.terms_offset_start && 22 | BigNumber(rune.chain_tip).lt(BigNumber(rune.block_height).plus(rune.terms_offset_start))) || 23 | (rune.terms_offset_end && 24 | BigNumber(rune.chain_tip).gt(BigNumber(rune.block_height).plus(rune.terms_offset_end))) 25 | ) { 26 | mintable = false; 27 | } 28 | return { 29 | id: rune.id, 30 | number: rune.number, 31 | name: rune.name, 32 | spaced_name: rune.spaced_name, 33 | divisibility: rune.divisibility, 34 | symbol: rune.symbol, 35 | mint_terms: { 36 | amount: rune.terms_amount ? divisibility(rune.terms_amount, rune.divisibility) : null, 37 | cap: rune.terms_cap ? divisibility(rune.terms_cap, rune.divisibility) : null, 38 | height_start: rune.terms_height_start ? parseInt(rune.terms_height_start) : null, 39 | height_end: rune.terms_height_end ? parseInt(rune.terms_height_end) : null, 40 | offset_start: rune.terms_offset_start ? parseInt(rune.terms_offset_start) : null, 41 | offset_end: rune.terms_offset_end ? parseInt(rune.terms_offset_end) : null, 42 | }, 43 | supply: { 44 | premine: divisibility(rune.premine, rune.divisibility), 45 | current: divisibility(BigNumber(minted).plus(burned).plus(rune.premine), rune.divisibility), 46 | minted: divisibility(minted, rune.divisibility), 47 | total_mints, 48 | burned: divisibility(burned, rune.divisibility), 49 | total_burns, 50 | mint_percentage: 51 | rune.terms_cap != null && rune.terms_cap != '0' 52 | ? BigNumber(total_mints).div(rune.terms_cap).times(100).toFixed(4) 53 | : '0.0000', 54 | mintable, 55 | }, 56 | turbo: rune.turbo, 57 | location: { 58 | block_hash: rune.block_hash, 59 | block_height: parseInt(rune.block_height), 60 | tx_index: rune.tx_index, 61 | tx_id: rune.tx_id, 62 | timestamp: rune.timestamp, 63 | }, 64 | }; 65 | } 66 | 67 | export function parseActivityResponse(entry: DbItemWithRune): ActivityResponse { 68 | return { 69 | rune: { 70 | id: entry.rune_id, 71 | number: entry.number, 72 | name: entry.name, 73 | spaced_name: entry.spaced_name, 74 | }, 75 | operation: entry.operation, 76 | address: entry.address ?? undefined, 77 | receiver_address: entry.receiver_address ?? undefined, 78 | amount: entry.amount ? divisibility(entry.amount, entry.divisibility) : undefined, 79 | location: { 80 | block_hash: entry.block_hash, 81 | block_height: parseInt(entry.block_height), 82 | tx_index: entry.tx_index, 83 | tx_id: entry.tx_id, 84 | vout: entry.output ?? undefined, 85 | output: entry.output ? `${entry.tx_id}:${entry.output}` : undefined, 86 | timestamp: entry.timestamp, 87 | }, 88 | }; 89 | } 90 | 91 | export function parseBalanceResponse(item: DbItemWithRune): BalanceResponse { 92 | return { 93 | rune: { 94 | id: item.rune_id, 95 | number: item.number, 96 | name: item.name, 97 | spaced_name: item.spaced_name, 98 | }, 99 | address: item.address, 100 | balance: divisibility(item.balance, item.divisibility), 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /api/runes/src/env.ts: -------------------------------------------------------------------------------- 1 | import { Static, Type } from '@sinclair/typebox'; 2 | import envSchema from 'env-schema'; 3 | 4 | const schema = Type.Object({ 5 | /** Hostname of the API server */ 6 | API_HOST: Type.String({ default: '0.0.0.0' }), 7 | /** Port in which to serve the API */ 8 | API_PORT: Type.Number({ default: 3000, minimum: 0, maximum: 65535 }), 9 | /** Port in which to serve the Admin RPC interface */ 10 | ADMIN_RPC_PORT: Type.Number({ default: 3001, minimum: 0, maximum: 65535 }), 11 | 12 | RUNES_PGHOST: Type.String(), 13 | RUNES_PGPORT: Type.Number({ default: 5432, minimum: 0, maximum: 65535 }), 14 | RUNES_PGUSER: Type.String(), 15 | RUNES_PGPASSWORD: Type.String(), 16 | RUNES_PGDATABASE: Type.String(), 17 | 18 | /** Limit to how many concurrent connections can be created */ 19 | PG_CONNECTION_POOL_MAX: Type.Number({ default: 10 }), 20 | PG_IDLE_TIMEOUT: Type.Number({ default: 30 }), 21 | PG_MAX_LIFETIME: Type.Number({ default: 60 }), 22 | PG_STATEMENT_TIMEOUT: Type.Number({ default: 60_000 }), 23 | }); 24 | type Env = Static; 25 | 26 | export const ENV = envSchema({ 27 | schema: schema, 28 | dotenv: true, 29 | }); 30 | -------------------------------------------------------------------------------- /api/runes/src/index.ts: -------------------------------------------------------------------------------- 1 | import { isProdEnv, logger, registerShutdownConfig } from '@hirosystems/api-toolkit'; 2 | import { buildApiServer, buildPrometheusServer } from './api/init'; 3 | import { ENV } from './env'; 4 | import { PgStore } from './pg/pg-store'; 5 | import { ApiMetrics } from './metrics/metrics'; 6 | 7 | async function initApiService(db: PgStore) { 8 | logger.info('Initializing API service...'); 9 | const fastify = await buildApiServer({ db }); 10 | registerShutdownConfig({ 11 | name: 'API Server', 12 | forceKillable: false, 13 | handler: async () => { 14 | await fastify.close(); 15 | }, 16 | }); 17 | 18 | await fastify.listen({ host: ENV.API_HOST, port: ENV.API_PORT }); 19 | 20 | if (isProdEnv) { 21 | const promServer = await buildPrometheusServer({ metrics: fastify.metrics }); 22 | registerShutdownConfig({ 23 | name: 'Prometheus Server', 24 | forceKillable: false, 25 | handler: async () => { 26 | await promServer.close(); 27 | }, 28 | }); 29 | ApiMetrics.configure(db); 30 | await promServer.listen({ host: ENV.API_HOST, port: 9153 }); 31 | } 32 | } 33 | 34 | async function initApp() { 35 | const db = await PgStore.connect(); 36 | await initApiService(db); 37 | 38 | registerShutdownConfig({ 39 | name: 'DB', 40 | forceKillable: false, 41 | handler: async () => { 42 | await db.close(); 43 | }, 44 | }); 45 | } 46 | 47 | registerShutdownConfig(); 48 | initApp() 49 | .then(() => { 50 | logger.info('App initialized'); 51 | }) 52 | .catch(error => { 53 | logger.error(error, `App failed to start`); 54 | process.exit(1); 55 | }); 56 | -------------------------------------------------------------------------------- /api/runes/src/metrics/metrics.ts: -------------------------------------------------------------------------------- 1 | import * as prom from 'prom-client'; 2 | import { PgStore } from '../pg/pg-store'; 3 | 4 | export class ApiMetrics { 5 | /** The most recent Bitcoin block height ingested by the API */ 6 | readonly runes_api_block_height: prom.Gauge; 7 | 8 | static configure(db: PgStore): ApiMetrics { 9 | return new ApiMetrics(db); 10 | } 11 | 12 | private constructor(db: PgStore) { 13 | this.runes_api_block_height = new prom.Gauge({ 14 | name: `runes_api_block_height`, 15 | help: 'The most recent Bitcoin block height ingested by the API', 16 | async collect() { 17 | const height = await db.getChainTipBlockHeight(); 18 | this.set(parseInt(height ?? '0')); 19 | }, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api/runes/src/pg/types.ts: -------------------------------------------------------------------------------- 1 | export type DbPaginatedResult = { 2 | total: number; 3 | results: T[]; 4 | }; 5 | 6 | export type DbCountedQueryResult = T & { total: number }; 7 | 8 | export type DbRune = { 9 | id: string; 10 | number: number; 11 | name: string; 12 | spaced_name: string; 13 | block_hash: string; 14 | block_height: string; 15 | tx_index: number; 16 | tx_id: string; 17 | divisibility: number; 18 | premine: string; 19 | symbol: string; 20 | cenotaph: boolean; 21 | terms_amount: string | null; 22 | terms_cap: string | null; 23 | terms_height_start: string | null; 24 | terms_height_end: string | null; 25 | terms_offset_start: string | null; 26 | terms_offset_end: string | null; 27 | turbo: boolean; 28 | minted: string | null; 29 | total_mints: string | null; 30 | burned: string | null; 31 | total_burns: string | null; 32 | total_operations: string | null; 33 | timestamp: number; 34 | }; 35 | 36 | export type DbRuneWithChainTip = DbRune & { chain_tip: string }; 37 | 38 | export type DbLedgerOperation = 'etching' | 'mint' | 'burn' | 'send' | 'receive'; 39 | 40 | export type DbLedgerEntry = { 41 | rune_id: string; 42 | block_hash: string; 43 | block_height: string; 44 | tx_index: number; 45 | tx_id: string; 46 | output: number | null; 47 | address: string | null; 48 | receiver_address: string | null; 49 | amount: string | null; 50 | operation: DbLedgerOperation; 51 | timestamp: number; 52 | }; 53 | 54 | export type DbItemWithRune = T & { 55 | name: string; 56 | number: number; 57 | spaced_name: string; 58 | divisibility: number; 59 | total_operations: number; 60 | }; 61 | 62 | export type DbBalance = { 63 | rune_id: string; 64 | address: string; 65 | balance: string; 66 | total_operations: number; 67 | }; 68 | -------------------------------------------------------------------------------- /api/runes/tests/setup.ts: -------------------------------------------------------------------------------- 1 | // ts-unused-exports:disable-next-line 2 | export default (): void => { 3 | process.env.API_HOST = '0.0.0.0'; 4 | process.env.API_PORT = '3000'; 5 | process.env.RUNES_PGHOST = '127.0.0.1'; 6 | process.env.RUNES_PGPORT = '5432'; 7 | process.env.RUNES_PGUSER = 'postgres'; 8 | process.env.RUNES_PGPASSWORD = 'postgres'; 9 | process.env.RUNES_PGDATABASE = 'postgres'; 10 | process.env.RUNES_SCHEMA = 'public'; 11 | }; 12 | -------------------------------------------------------------------------------- /api/runes/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "tests/**/*.ts", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /api/runes/util/openapi-generator.ts: -------------------------------------------------------------------------------- 1 | import Fastify, { FastifyPluginAsync } from 'fastify'; 2 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; 3 | import { Api } from '../src/api/init'; 4 | import FastifySwagger from '@fastify/swagger'; 5 | import { existsSync, mkdirSync, writeFileSync } from 'fs'; 6 | import { Server } from 'http'; 7 | import { OpenApiSchemaOptions } from '../src/api/schemas'; 8 | 9 | /** 10 | * Generates `openapi.yaml` based on current Swagger definitions. 11 | */ 12 | export const ApiGenerator: FastifyPluginAsync< 13 | Record, 14 | Server, 15 | TypeBoxTypeProvider 16 | > = async (fastify, options) => { 17 | await fastify.register(FastifySwagger, OpenApiSchemaOptions); 18 | await fastify.register(Api, { prefix: '/runes/v1' }); 19 | if (!existsSync('./tmp')) { 20 | mkdirSync('./tmp'); 21 | } 22 | fastify.addHook('onReady', () => { 23 | writeFileSync('./tmp/openapi.yaml', fastify.swagger({ yaml: true })); 24 | writeFileSync('./tmp/openapi.json', JSON.stringify(fastify.swagger(), null, 2)); 25 | }); 26 | }; 27 | 28 | const fastify = Fastify({ 29 | trustProxy: true, 30 | logger: true, 31 | }).withTypeProvider(); 32 | 33 | void fastify.register(ApiGenerator).then(async () => { 34 | await fastify.ready(); 35 | await fastify.close(); 36 | }); 37 | -------------------------------------------------------------------------------- /components/bitcoind/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitcoind" 3 | version = "1.0.0" 4 | description = "Stateless Transaction Indexing Engine for Bitcoin" 5 | license = "GPL-3.0" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | config = { path = "../config" } 10 | serde = { version = "1", features = ["rc"] } 11 | serde_json = { version = "1", features = ["arbitrary_precision"] } 12 | serde-hex = "0.1.0" 13 | serde_derive = "1" 14 | hiro-system-kit = { workspace = true } 15 | rocket = { version = "=0.5.0", features = ["json"] } 16 | bitcoin = { workspace = true } 17 | bitcoincore-rpc = "0.18.0" 18 | bitcoincore-rpc-json = "0.18.0" 19 | reqwest = { version = "0.12", default-features = false, features = [ 20 | "blocking", 21 | "json", 22 | "rustls-tls", 23 | ] } 24 | tokio = { workspace = true } 25 | base58 = "0.2.0" 26 | crossbeam-channel = "0.5.6" 27 | hex = "0.4.3" 28 | zmq = "0.10.0" 29 | lazy_static = "1.4.0" 30 | schemars = { version = "0.8.16", git = "https://github.com/hirosystems/schemars.git", branch = "feat-chainhook-fixes" } 31 | strum = { version = "0.23.0", features = ["derive"] } 32 | 33 | [dev-dependencies] 34 | assert-json-diff = "2.0.2" 35 | test-case = "3.1.0" 36 | 37 | [features] 38 | default = ["hiro-system-kit/log"] 39 | debug = ["hiro-system-kit/debug"] 40 | release = ["hiro-system-kit/release_debug", "hiro-system-kit/full_log_level_prefix"] 41 | -------------------------------------------------------------------------------- /components/bitcoind/src/indexer/tests/helpers/accounts.rs: -------------------------------------------------------------------------------- 1 | pub fn deployer_stx_address() -> String { 2 | "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".to_string() 3 | } 4 | 5 | pub fn wallet_1_stx_address() -> String { 6 | "ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5".to_string() 7 | } 8 | 9 | pub fn wallet_2_stx_address() -> String { 10 | "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG".to_string() 11 | } 12 | 13 | pub fn wallet_3_stx_address() -> String { 14 | "ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC".to_string() 15 | } 16 | 17 | pub fn wallet_4_stx_address() -> String { 18 | "ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND".to_string() 19 | } 20 | 21 | pub fn wallet_5_stx_address() -> String { 22 | "ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB".to_string() 23 | } 24 | 25 | pub fn wallet_6_stx_address() -> String { 26 | "ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0".to_string() 27 | } 28 | 29 | pub fn wallet_7_stx_address() -> String { 30 | "ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ".to_string() 31 | } 32 | 33 | pub fn wallet_8_stx_address() -> String { 34 | "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP".to_string() 35 | } 36 | 37 | pub fn wallet_9_stx_address() -> String { 38 | "STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6".to_string() 39 | } 40 | 41 | pub fn deployer_btc_address() -> String { 42 | "mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH".to_string() 43 | } 44 | 45 | pub fn wallet_1_btc_address() -> String { 46 | "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC".to_string() 47 | } 48 | 49 | pub fn wallet_2_btc_address() -> String { 50 | "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG".to_string() 51 | } 52 | 53 | pub fn wallet_3_btc_address() -> String { 54 | "mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7".to_string() 55 | } 56 | 57 | pub fn wallet_4_btc_address() -> String { 58 | "mg1C76bNTutiCDV3t9nWhZs3Dc8LzUufj8".to_string() 59 | } 60 | 61 | pub fn wallet_5_btc_address() -> String { 62 | "mweN5WVqadScHdA81aATSdcVr4B6dNokqx".to_string() 63 | } 64 | 65 | pub fn wallet_6_btc_address() -> String { 66 | "mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt".to_string() 67 | } 68 | 69 | pub fn wallet_7_btc_address() -> String { 70 | "n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7".to_string() 71 | } 72 | 73 | pub fn wallet_8_btc_address() -> String { 74 | "n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw".to_string() 75 | } 76 | 77 | pub fn wallet_9_btc_address() -> String { 78 | "mjSrB3wS4xab3kYqFktwBzfTdPg367ZJ2d".to_string() 79 | } 80 | -------------------------------------------------------------------------------- /components/bitcoind/src/indexer/tests/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod accounts; 2 | #[allow(non_snake_case, unreachable_code)] 3 | pub mod bitcoin_blocks; 4 | pub mod bitcoin_shapes; 5 | pub mod transactions; 6 | -------------------------------------------------------------------------------- /components/bitcoind/src/indexer/tests/helpers/transactions.rs: -------------------------------------------------------------------------------- 1 | use base58::FromBase58; 2 | use bitcoincore_rpc::bitcoin::blockdata::{opcodes, script::Builder as BitcoinScriptBuilder}; 3 | 4 | use crate::types::{ 5 | bitcoin::TxOut, BitcoinTransactionData, BitcoinTransactionMetadata, TransactionIdentifier, 6 | }; 7 | 8 | pub fn generate_test_tx_bitcoin_p2pkh_transfer( 9 | txid: u64, 10 | _sender: &str, 11 | recipient: &str, 12 | amount: u64, 13 | ) -> BitcoinTransactionData { 14 | let mut hash = vec![ 15 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 | ]; 17 | hash.append(&mut txid.to_be_bytes().to_vec()); 18 | 19 | // Preparing metadata 20 | let pubkey_hash = recipient 21 | .from_base58() 22 | .expect("Unable to get bytes from btc address"); 23 | let slice = [ 24 | pubkey_hash[1], 25 | pubkey_hash[2], 26 | pubkey_hash[3], 27 | pubkey_hash[4], 28 | pubkey_hash[5], 29 | pubkey_hash[6], 30 | pubkey_hash[7], 31 | pubkey_hash[8], 32 | pubkey_hash[9], 33 | pubkey_hash[10], 34 | pubkey_hash[11], 35 | pubkey_hash[12], 36 | pubkey_hash[13], 37 | pubkey_hash[14], 38 | pubkey_hash[15], 39 | pubkey_hash[16], 40 | pubkey_hash[17], 41 | pubkey_hash[18], 42 | pubkey_hash[19], 43 | pubkey_hash[20], 44 | ]; 45 | let script = BitcoinScriptBuilder::new() 46 | .push_opcode(opcodes::all::OP_DUP) 47 | .push_opcode(opcodes::all::OP_HASH160) 48 | .push_slice(slice) 49 | .push_opcode(opcodes::all::OP_EQUALVERIFY) 50 | .push_opcode(opcodes::all::OP_CHECKSIG) 51 | .into_script(); 52 | let outputs = vec![TxOut { 53 | value: amount, 54 | script_pubkey: format!("0x{}", hex::encode(script.as_bytes())), 55 | }]; 56 | 57 | BitcoinTransactionData { 58 | transaction_identifier: TransactionIdentifier { 59 | hash: format!("0x{}", hex::encode(&hash[..])), 60 | }, 61 | operations: vec![], 62 | metadata: BitcoinTransactionMetadata { 63 | inputs: vec![], 64 | outputs, 65 | ordinal_operations: vec![], 66 | brc20_operation: None, 67 | proof: None, 68 | fee: 0, 69 | index: 0, 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /components/bitcoind/src/indexer/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod helpers; 2 | use super::fork_scratch_pad::ForkScratchPad; 3 | use crate::{ 4 | types::{BitcoinBlockData, BlockchainEvent}, 5 | utils::{AbstractBlock, Context}, 6 | }; 7 | 8 | pub type BlockchainEventExpectation = Box)>; 9 | 10 | pub fn process_bitcoin_blocks_and_check_expectations( 11 | steps: Vec<(BitcoinBlockData, BlockchainEventExpectation)>, 12 | ) { 13 | let mut blocks_processor = ForkScratchPad::new(); 14 | for (block, check_chain_event_expectations) in steps.into_iter() { 15 | let chain_event = blocks_processor 16 | .process_header(block.get_header(), &Context::empty()) 17 | .unwrap(); 18 | check_chain_event_expectations(chain_event); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/bitcoind/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate serde; 2 | 3 | #[macro_use] 4 | extern crate serde_derive; 5 | 6 | #[macro_use] 7 | extern crate serde_json; 8 | 9 | pub use bitcoincore_rpc; 10 | 11 | pub mod indexer; 12 | pub mod observer; 13 | pub mod types; 14 | pub mod utils; 15 | -------------------------------------------------------------------------------- /components/bitcoind/src/observer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod zmq; 2 | -------------------------------------------------------------------------------- /components/bitcoind/src/observer/zmq.rs: -------------------------------------------------------------------------------- 1 | use config::Config; 2 | use zmq::Socket; 3 | 4 | use crate::{ 5 | indexer::{ 6 | bitcoin::{ 7 | build_http_client, cursor::BlockBytesCursor, download_and_parse_block_with_retry, 8 | standardize_bitcoin_block, 9 | }, 10 | BlockProcessor, BlockProcessorCommand, 11 | }, 12 | try_info, try_warn, 13 | types::BitcoinNetwork, 14 | utils::Context, 15 | }; 16 | 17 | fn new_zmq_socket() -> Socket { 18 | let context = zmq::Context::new(); 19 | let socket = context.socket(zmq::SUB).unwrap(); 20 | assert!(socket.set_subscribe(b"hashblock").is_ok()); 21 | assert!(socket.set_rcvhwm(0).is_ok()); 22 | // We override the OS default behavior: 23 | assert!(socket.set_tcp_keepalive(1).is_ok()); 24 | // The keepalive routine will wait for 5 minutes 25 | assert!(socket.set_tcp_keepalive_idle(300).is_ok()); 26 | // And then resend it every 60 seconds 27 | assert!(socket.set_tcp_keepalive_intvl(60).is_ok()); 28 | // 120 times 29 | assert!(socket.set_tcp_keepalive_cnt(120).is_ok()); 30 | socket 31 | } 32 | 33 | pub async fn start_zeromq_pipeline( 34 | blocks_post_processor: &BlockProcessor, 35 | start_sequencing_blocks_at_height: u64, 36 | compress_blocks: bool, 37 | config: &Config, 38 | ctx: &Context, 39 | ) -> Result<(), String> { 40 | let http_client = build_http_client(); 41 | let bitcoind_zmq_url = config.bitcoind.zmq_url.clone(); 42 | let network = BitcoinNetwork::from_network(config.bitcoind.network); 43 | try_info!( 44 | ctx, 45 | "zmq: Waiting for ZMQ connection acknowledgment from bitcoind" 46 | ); 47 | 48 | let mut socket = new_zmq_socket(); 49 | assert!(socket.connect(&bitcoind_zmq_url).is_ok()); 50 | try_info!( 51 | ctx, 52 | "zmq: Connected, waiting for ZMQ messages from bitcoind" 53 | ); 54 | 55 | loop { 56 | let msg = match socket.recv_multipart(0) { 57 | Ok(msg) => msg, 58 | Err(e) => { 59 | try_warn!(ctx, "zmq: Unable to receive ZMQ message: {e}"); 60 | socket = new_zmq_socket(); 61 | assert!(socket.connect(&bitcoind_zmq_url).is_ok()); 62 | continue; 63 | } 64 | }; 65 | let (topic, data, _sequence) = (&msg[0], &msg[1], &msg[2]); 66 | 67 | if !topic.eq(b"hashblock") { 68 | try_warn!( 69 | ctx, 70 | "zmq: {} Topic not supported", 71 | String::from_utf8(topic.clone()).unwrap() 72 | ); 73 | continue; 74 | } 75 | 76 | let block_hash = hex::encode(data); 77 | 78 | try_info!(ctx, "zmq: Bitcoin block hash announced {block_hash}"); 79 | let raw_block_data = match download_and_parse_block_with_retry( 80 | &http_client, 81 | &block_hash, 82 | &config.bitcoind, 83 | ctx, 84 | ) 85 | .await 86 | { 87 | Ok(block) => block, 88 | Err(e) => { 89 | try_warn!(ctx, "zmq: Unable to download block: {e}"); 90 | continue; 91 | } 92 | }; 93 | let block_height = raw_block_data.height as u64; 94 | let compacted_blocks = if compress_blocks { 95 | vec![( 96 | block_height, 97 | BlockBytesCursor::from_full_block(&raw_block_data) 98 | .expect("unable to compress block"), 99 | )] 100 | } else { 101 | vec![] 102 | }; 103 | let blocks = if block_height >= start_sequencing_blocks_at_height { 104 | let block = standardize_bitcoin_block(raw_block_data, &network, ctx) 105 | .expect("unable to deserialize block"); 106 | vec![block] 107 | } else { 108 | vec![] 109 | }; 110 | blocks_post_processor 111 | .commands_tx 112 | .send(BlockProcessorCommand::ProcessBlocks { 113 | compacted_blocks, 114 | blocks, 115 | }) 116 | .map_err(|e| e.to_string())?; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /components/bitcoind/src/types/bitcoin.rs: -------------------------------------------------------------------------------- 1 | use super::TransactionIdentifier; 2 | 3 | /// A transaction input, which defines old coins to be consumed 4 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)] 5 | pub struct TxIn { 6 | /// The reference to the previous output that is being used an an input. 7 | pub previous_output: OutPoint, 8 | /// The script which pushes values on the stack which will cause 9 | /// the referenced output's script to be accepted. 10 | pub script_sig: String, 11 | /// The sequence number, which suggests to miners which of two 12 | /// conflicting transactions should be preferred, or 0xFFFFFFFF 13 | /// to ignore this feature. This is generally never used since 14 | /// the miner behaviour cannot be enforced. 15 | pub sequence: u32, 16 | /// Witness data: an array of byte-arrays. 17 | /// Note that this field is *not* (de)serialized with the rest of the TxIn in 18 | /// Encodable/Decodable, as it is (de)serialized at the end of the full 19 | /// Transaction. It *is* (de)serialized with the rest of the TxIn in other 20 | /// (de)serialization routines. 21 | pub witness: Vec, 22 | } 23 | 24 | /// A transaction output, which defines new coins to be created from old ones. 25 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)] 26 | pub struct TxOut { 27 | /// The value of the output, in satoshis. 28 | pub value: u64, 29 | /// The script which must be satisfied for the output to be spent. 30 | pub script_pubkey: String, 31 | } 32 | 33 | /// A reference to a transaction output. 34 | #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] 35 | pub struct OutPoint { 36 | /// The referenced transaction's txid. 37 | pub txid: TransactionIdentifier, 38 | /// The index of the referenced output in its transaction's vout. 39 | pub vout: u32, 40 | /// The value of the referenced. 41 | pub value: u64, 42 | /// The script which must be satisfied for the output to be spent. 43 | pub block_height: u64, 44 | } 45 | 46 | impl TxOut { 47 | pub fn get_script_pubkey_bytes(&self) -> Vec { 48 | hex::decode(self.get_script_pubkey_hex()).expect("not provided for coinbase txs") 49 | } 50 | 51 | pub fn get_script_pubkey_hex(&self) -> &str { 52 | &self.script_pubkey[2..] 53 | } 54 | } 55 | 56 | /// The Witness is the data used to unlock bitcoins since the [segwit upgrade](https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki) 57 | /// 58 | /// Can be logically seen as an array of byte-arrays `Vec>` and indeed you can convert from 59 | /// it [`Witness::from_vec`] and convert into it [`Witness::to_vec`]. 60 | /// 61 | /// For serialization and deserialization performance it is stored internally as a single `Vec`, 62 | /// saving some allocations. 63 | /// 64 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)] 65 | pub struct Witness { 66 | /// contains the witness `Vec>` serialization without the initial varint indicating the 67 | /// number of elements (which is stored in `witness_elements`) 68 | content: Vec, 69 | 70 | /// Number of elements in the witness. 71 | /// It is stored separately (instead of as VarInt in the initial part of content) so that method 72 | /// like [`Witness::push`] doesn't have case requiring to shift the entire array 73 | witness_elements: usize, 74 | 75 | /// If `witness_elements > 0` it's a valid index pointing to the last witness element in `content` 76 | /// (Including the varint specifying the length of the element) 77 | last: usize, 78 | 79 | /// If `witness_elements > 1` it's a valid index pointing to the second-to-last witness element in `content` 80 | /// (Including the varint specifying the length of the element) 81 | second_to_last: usize, 82 | } 83 | -------------------------------------------------------------------------------- /components/bitcoind/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bitcoin; 2 | mod ordinals; 3 | mod processors; 4 | mod rosetta; 5 | 6 | pub use ordinals::*; 7 | pub use processors::*; 8 | pub use rosetta::*; 9 | 10 | #[derive(Clone, Debug)] 11 | pub enum Chain { 12 | Bitcoin, 13 | } 14 | -------------------------------------------------------------------------------- /components/bitcoind/src/types/ordinals.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 4 | #[serde(rename_all = "snake_case")] 5 | pub enum OrdinalOperation { 6 | InscriptionRevealed(OrdinalInscriptionRevealData), 7 | InscriptionTransferred(OrdinalInscriptionTransferData), 8 | } 9 | 10 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 11 | pub struct OrdinalInscriptionTransferData { 12 | pub ordinal_number: u64, 13 | pub destination: OrdinalInscriptionTransferDestination, 14 | pub satpoint_pre_transfer: String, 15 | pub satpoint_post_transfer: String, 16 | pub post_transfer_output_value: Option, 17 | pub tx_index: usize, 18 | } 19 | 20 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 21 | #[serde(tag = "type", content = "value", rename_all = "snake_case")] 22 | pub enum OrdinalInscriptionTransferDestination { 23 | Transferred(String), 24 | SpentInFees, 25 | Burnt(String), 26 | } 27 | 28 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 29 | pub enum OrdinalInscriptionCurseType { 30 | DuplicateField, 31 | IncompleteField, 32 | NotAtOffsetZero, 33 | NotInFirstInput, 34 | Pointer, 35 | Pushnum, 36 | Reinscription, 37 | Stutter, 38 | UnrecognizedEvenField, 39 | Generic, 40 | } 41 | 42 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 43 | pub struct OrdinalInscriptionRevealData { 44 | pub content_bytes: String, 45 | pub content_type: String, 46 | pub content_length: usize, 47 | pub inscription_number: OrdinalInscriptionNumber, 48 | pub inscription_fee: u64, 49 | pub inscription_output_value: u64, 50 | pub inscription_id: String, 51 | pub inscription_input_index: usize, 52 | pub inscription_pointer: Option, 53 | pub inscriber_address: Option, 54 | pub delegate: Option, 55 | pub metaprotocol: Option, 56 | pub metadata: Option, 57 | pub parents: Vec, 58 | pub ordinal_number: u64, 59 | pub ordinal_block_height: u64, 60 | pub ordinal_offset: u64, 61 | pub tx_index: usize, 62 | pub transfers_pre_inscription: u32, 63 | pub satpoint_post_inscription: String, 64 | pub curse_type: Option, 65 | pub charms: u16, 66 | pub unbound_sequence: Option, 67 | } 68 | 69 | impl OrdinalInscriptionNumber { 70 | pub fn zero() -> Self { 71 | OrdinalInscriptionNumber { 72 | jubilee: 0, 73 | classic: 0, 74 | } 75 | } 76 | } 77 | 78 | impl OrdinalInscriptionRevealData { 79 | pub fn get_inscription_number(&self) -> i64 { 80 | self.inscription_number.jubilee 81 | } 82 | } 83 | 84 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 85 | pub struct OrdinalInscriptionNumber { 86 | pub classic: i64, 87 | pub jubilee: i64, 88 | } 89 | 90 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 91 | pub struct Brc20TokenDeployData { 92 | pub tick: String, 93 | pub max: String, 94 | pub lim: String, 95 | pub dec: String, 96 | pub address: String, 97 | pub inscription_id: String, 98 | pub self_mint: bool, 99 | } 100 | 101 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 102 | pub struct Brc20BalanceData { 103 | pub tick: String, 104 | pub amt: String, 105 | pub address: String, 106 | pub inscription_id: String, 107 | } 108 | 109 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 110 | pub struct Brc20TransferData { 111 | pub tick: String, 112 | pub amt: String, 113 | pub sender_address: String, 114 | pub receiver_address: String, 115 | pub inscription_id: String, 116 | } 117 | 118 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 119 | #[serde(rename_all = "snake_case")] 120 | pub enum Brc20Operation { 121 | Deploy(Brc20TokenDeployData), 122 | Mint(Brc20BalanceData), 123 | Transfer(Brc20BalanceData), 124 | TransferSend(Brc20TransferData), 125 | } 126 | -------------------------------------------------------------------------------- /components/bitcoind/src/types/processors.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use serde_json::Value as JsonValue; 4 | 5 | use super::{BitcoinBlockData, BitcoinTransactionData}; 6 | 7 | pub struct ProcessedBitcoinTransaction { 8 | pub tx: BitcoinTransactionData, 9 | pub metadata: BTreeMap, 10 | } 11 | 12 | pub struct ProcessedBitcoinBlock { 13 | pub tx: BitcoinBlockData, 14 | pub metadata: BTreeMap, 15 | } 16 | 17 | pub enum ProcessingContext { 18 | Scanning, 19 | Streaming, 20 | } 21 | 22 | pub trait BitcoinProtocolProcessor { 23 | fn register(&mut self); 24 | fn process_block( 25 | &mut self, 26 | block: &mut ProcessedBitcoinBlock, 27 | processing_context: ProcessingContext, 28 | ); 29 | fn process_transaction( 30 | &mut self, 31 | transaction: &mut ProcessedBitcoinTransaction, 32 | processing_context: ProcessingContext, 33 | ); 34 | } 35 | 36 | pub fn run_processor

(mut p: P) 37 | where 38 | P: BitcoinProtocolProcessor, 39 | { 40 | p.register(); 41 | } 42 | -------------------------------------------------------------------------------- /components/bitcoind/src/utils/bitcoind.rs: -------------------------------------------------------------------------------- 1 | use std::{thread::sleep, time::Duration}; 2 | 3 | use bitcoincore_rpc::{Auth, Client, RpcApi}; 4 | use config::BitcoindConfig; 5 | 6 | use crate::{try_error, try_info, types::BlockIdentifier, utils::Context}; 7 | 8 | fn bitcoind_get_client(config: &BitcoindConfig, ctx: &Context) -> Client { 9 | loop { 10 | let auth = Auth::UserPass(config.rpc_username.clone(), config.rpc_password.clone()); 11 | match Client::new(&config.rpc_url, auth) { 12 | Ok(con) => { 13 | return con; 14 | } 15 | Err(e) => { 16 | try_error!(ctx, "bitcoind: Unable to get client: {}", e.to_string()); 17 | sleep(Duration::from_secs(1)); 18 | } 19 | } 20 | } 21 | } 22 | 23 | /// Retrieves the chain tip from bitcoind. 24 | pub fn bitcoind_get_chain_tip(config: &BitcoindConfig, ctx: &Context) -> BlockIdentifier { 25 | let bitcoin_rpc = bitcoind_get_client(config, ctx); 26 | loop { 27 | match bitcoin_rpc.get_blockchain_info() { 28 | Ok(result) => { 29 | return BlockIdentifier { 30 | index: result.blocks, 31 | hash: format!("0x{}", result.best_block_hash), 32 | }; 33 | } 34 | Err(e) => { 35 | try_error!( 36 | ctx, 37 | "bitcoind: Unable to get block height: {}", 38 | e.to_string() 39 | ); 40 | sleep(Duration::from_secs(1)); 41 | } 42 | }; 43 | } 44 | } 45 | 46 | /// Checks if bitcoind is still synchronizing blocks and waits until it's finished if that is the case. 47 | pub fn bitcoind_wait_for_chain_tip(config: &BitcoindConfig, ctx: &Context) -> BlockIdentifier { 48 | let bitcoin_rpc = bitcoind_get_client(config, ctx); 49 | let mut confirmations = 0; 50 | loop { 51 | match bitcoin_rpc.get_blockchain_info() { 52 | Ok(result) => { 53 | if !result.initial_block_download && result.blocks == result.headers { 54 | confirmations += 1; 55 | // Wait for 10 confirmations before declaring node is at chain tip, just in case it's still connecting to 56 | // peers. 57 | if confirmations == 10 { 58 | try_info!(ctx, "bitcoind: Chain tip reached"); 59 | return BlockIdentifier { 60 | index: result.blocks, 61 | hash: format!("0x{}", result.best_block_hash), 62 | }; 63 | } 64 | try_info!(ctx, "bitcoind: Verifying chain tip"); 65 | } else { 66 | confirmations = 0; 67 | try_info!( 68 | ctx, 69 | "bitcoind: Node has not reached chain tip, trying again" 70 | ); 71 | } 72 | } 73 | Err(e) => { 74 | try_error!( 75 | ctx, 76 | "bitcoind: Unable to check for chain tip: {}", 77 | e.to_string() 78 | ); 79 | } 80 | }; 81 | sleep(Duration::from_secs(1)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /components/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli" 3 | version.workspace = true 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [[bin]] 8 | name = "bitcoin-indexer" 9 | path = "src/main.rs" 10 | 11 | [lib] 12 | path = "src/lib.rs" 13 | 14 | [dependencies] 15 | config = { path = "../config" } 16 | ordinals = { path = "../ordinals" } 17 | runes = { path = "../runes" } 18 | bitcoind = { path = "../bitcoind" } 19 | hex = "0.4.3" 20 | num_cpus = "1.16.0" 21 | serde = "1" 22 | serde_json = "1" 23 | serde_derive = "1" 24 | reqwest = { version = "0.11", default-features = false, features = [ 25 | "stream", 26 | "json", 27 | "rustls-tls", 28 | ] } 29 | hiro-system-kit = { workspace = true } 30 | clap = { version = "3.2.23", features = ["derive"], optional = true } 31 | clap_generate = { version = "3.0.3", optional = true } 32 | toml = { version = "0.5.6", features = ["preserve_order"], optional = true } 33 | ctrlc = { version = "3.2.2", optional = true } 34 | tcmalloc2 = { version = "0.1.2", optional = true } 35 | 36 | [features] 37 | default = ["cli"] 38 | cli = ["clap", "clap_generate", "toml", "ctrlc", "hiro-system-kit/log"] 39 | debug = ["hiro-system-kit/debug"] 40 | release = ["hiro-system-kit/release"] 41 | tcmalloc = ["tcmalloc2"] -------------------------------------------------------------------------------- /components/cli/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // note: add error checking yourself. 3 | } 4 | -------------------------------------------------------------------------------- /components/cli/src/cli/commands.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | /// Index Bitcoin meta-protocols like Ordinals, BRC-20, and Runes 4 | #[derive(Parser, Debug)] 5 | #[clap(name = "bitcoin-indexer", author, version, about, long_about = None)] 6 | pub enum Protocol { 7 | /// Ordinals index commands 8 | #[clap(subcommand)] 9 | Ordinals(Command), 10 | /// Runes index commands 11 | #[clap(subcommand)] 12 | Runes(Command), 13 | /// Configuration file commands 14 | #[clap(subcommand)] 15 | Config(ConfigCommand), 16 | } 17 | 18 | #[derive(Subcommand, PartialEq, Clone, Debug)] 19 | pub enum Command { 20 | /// Stream and index Bitcoin blocks 21 | #[clap(subcommand)] 22 | Service(ServiceCommand), 23 | /// Perform maintenance operations on local index 24 | #[clap(subcommand)] 25 | Index(IndexCommand), 26 | /// Database operations 27 | #[clap(subcommand)] 28 | Database(DatabaseCommand), 29 | } 30 | 31 | #[derive(Subcommand, PartialEq, Clone, Debug)] 32 | pub enum DatabaseCommand { 33 | /// Migrates database 34 | #[clap(name = "migrate", bin_name = "migrate")] 35 | Migrate(MigrateDatabaseCommand), 36 | } 37 | 38 | #[derive(Parser, PartialEq, Clone, Debug)] 39 | pub struct MigrateDatabaseCommand { 40 | #[clap(long = "config-path")] 41 | pub config_path: String, 42 | } 43 | 44 | #[derive(Subcommand, PartialEq, Clone, Debug)] 45 | #[clap(bin_name = "config", aliases = &["config"])] 46 | pub enum ConfigCommand { 47 | /// Generate new config 48 | #[clap(name = "new", bin_name = "new", aliases = &["generate"])] 49 | New(NewConfigCommand), 50 | } 51 | 52 | #[derive(Parser, PartialEq, Clone, Debug)] 53 | pub struct NewConfigCommand { 54 | /// Target Regtest network 55 | #[clap( 56 | long = "regtest", 57 | conflicts_with = "testnet", 58 | conflicts_with = "mainnet" 59 | )] 60 | pub regtest: bool, 61 | /// Target Testnet network 62 | #[clap( 63 | long = "testnet", 64 | conflicts_with = "regtest", 65 | conflicts_with = "mainnet" 66 | )] 67 | pub testnet: bool, 68 | /// Target Mainnet network 69 | #[clap( 70 | long = "mainnet", 71 | conflicts_with = "testnet", 72 | conflicts_with = "regtest" 73 | )] 74 | pub mainnet: bool, 75 | } 76 | 77 | #[derive(Subcommand, PartialEq, Clone, Debug)] 78 | pub enum ServiceCommand { 79 | /// Start service 80 | #[clap(name = "start", bin_name = "start")] 81 | Start(ServiceStartCommand), 82 | } 83 | 84 | #[derive(Parser, PartialEq, Clone, Debug)] 85 | pub struct ServiceStartCommand { 86 | #[clap(long = "config-path")] 87 | pub config_path: String, 88 | } 89 | 90 | #[derive(Subcommand, PartialEq, Clone, Debug)] 91 | pub enum IndexCommand { 92 | /// Sync index to latest bitcoin block 93 | #[clap(name = "sync", bin_name = "sync")] 94 | Sync(SyncIndexCommand), 95 | /// Rollback index blocks 96 | #[clap(name = "rollback", bin_name = "drop")] 97 | Rollback(RollbackIndexCommand), 98 | } 99 | 100 | #[derive(Parser, PartialEq, Clone, Debug)] 101 | pub struct SyncIndexCommand { 102 | #[clap(long = "config-path")] 103 | pub config_path: String, 104 | } 105 | 106 | #[derive(Parser, PartialEq, Clone, Debug)] 107 | pub struct RollbackIndexCommand { 108 | /// Number of blocks to rollback from index tip 109 | pub blocks: u32, 110 | #[clap(long = "config-path")] 111 | pub config_path: String, 112 | } 113 | -------------------------------------------------------------------------------- /components/cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Re-export modules so they can be tested 2 | pub mod cli; 3 | -------------------------------------------------------------------------------- /components/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | 3 | extern crate hiro_system_kit; 4 | 5 | #[cfg(feature = "tcmalloc")] 6 | #[global_allocator] 7 | static GLOBAL: tcmalloc2::TcMalloc = tcmalloc2::TcMalloc; 8 | 9 | fn main() { 10 | cli::main(); 11 | } 12 | -------------------------------------------------------------------------------- /components/config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "config" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | bitcoin = { workspace = true } 8 | serde = "1" 9 | serde_json = "1" 10 | serde_derive = "1" 11 | hiro-system-kit = { workspace = true } 12 | num_cpus = "1.16.0" 13 | toml = { version = "0.5.11", features = ["preserve_order"] } 14 | -------------------------------------------------------------------------------- /components/config/src/generator.rs: -------------------------------------------------------------------------------- 1 | pub fn generate_toml_config(network: &str) -> String { 2 | let conf = format!( 3 | r#"[storage] 4 | working_dir = "tmp" 5 | 6 | [metrics] 7 | enabled = true 8 | prometheus_port = 9153 9 | 10 | [ordinals.db] 11 | database = "ordinals" 12 | host = "localhost" 13 | port = 5432 14 | username = "postgres" 15 | password = "postgres" 16 | 17 | [ordinals.meta_protocols.brc20] 18 | enabled = true 19 | lru_cache_size = 10000 20 | 21 | [ordinals.meta_protocols.brc20.db] 22 | database = "brc20" 23 | host = "localhost" 24 | port = 5432 25 | username = "postgres" 26 | password = "postgres" 27 | 28 | [runes] 29 | lru_cache_size = 10000 30 | 31 | [runes.db] 32 | database = "runes" 33 | host = "localhost" 34 | port = 5432 35 | username = "postgres" 36 | password = "postgres" 37 | 38 | [bitcoind] 39 | network = "{network}" 40 | rpc_url = "http://localhost:8332" 41 | rpc_username = "devnet" 42 | rpc_password = "devnet" 43 | zmq_url = "tcp://0.0.0.0:18543" 44 | 45 | [resources] 46 | ulimit = 2048 47 | cpu_core_available = 6 48 | memory_available = 16 49 | bitcoind_rpc_threads = 2 50 | bitcoind_rpc_timeout = 15 51 | "#, 52 | network = network.to_lowercase(), 53 | ); 54 | conf 55 | } 56 | -------------------------------------------------------------------------------- /components/config/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | pub mod generator; 5 | pub mod toml; 6 | 7 | mod config; 8 | pub use config::*; 9 | -------------------------------------------------------------------------------- /components/ord/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ord" 3 | version = "0.22.2" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = { version = "1.0.56", features = ["backtrace"] } 8 | bitcoin = { workspace = true } 9 | bitcoind = { path = "../bitcoind" } 10 | ciborium = "0.2.1" 11 | serde = "1" 12 | serde_derive = "1" 13 | serde_json = "1" 14 | -------------------------------------------------------------------------------- /components/ord/README.md: -------------------------------------------------------------------------------- 1 | This code is manually imported from [ordinals/ord](https://github.com/ordinals/ord) and it is used for all ordinal inscription parsing. -------------------------------------------------------------------------------- /components/ord/src/decimal_sat.rs: -------------------------------------------------------------------------------- 1 | use super::{height::Height, sat::Sat}; 2 | 3 | #[derive(PartialEq, Debug)] 4 | pub struct DecimalSat { 5 | pub height: Height, 6 | pub offset: u64, 7 | } 8 | 9 | impl From for DecimalSat { 10 | fn from(sat: Sat) -> Self { 11 | Self { 12 | height: sat.height(), 13 | offset: sat.third(), 14 | } 15 | } 16 | } 17 | 18 | // impl Display for DecimalSat { 19 | // fn fmt(&self, f: &mut Formatter) -> fmt::Result { 20 | // write!(f, "{}.{}", self.height, self.offset) 21 | // } 22 | // } 23 | 24 | // #[cfg(test)] 25 | // mod tests { 26 | // use super::*; 27 | 28 | // #[test] 29 | // fn decimal() { 30 | // assert_eq!( 31 | // Sat(0).decimal(), 32 | // DecimalSat { 33 | // height: Height(0), 34 | // offset: 0 35 | // } 36 | // ); 37 | // assert_eq!( 38 | // Sat(1).decimal(), 39 | // DecimalSat { 40 | // height: Height(0), 41 | // offset: 1 42 | // } 43 | // ); 44 | // assert_eq!( 45 | // Sat(2099999997689999).decimal(), 46 | // DecimalSat { 47 | // height: Height(6929999), 48 | // offset: 0 49 | // } 50 | // ); 51 | // } 52 | // } 53 | -------------------------------------------------------------------------------- /components/ord/src/degree.rs: -------------------------------------------------------------------------------- 1 | use super::{sat::Sat, *}; 2 | 3 | #[derive(PartialEq, Debug)] 4 | pub struct Degree { 5 | pub hour: u32, 6 | pub minute: u32, 7 | pub second: u32, 8 | pub third: u64, 9 | } 10 | 11 | // impl Display for Degree { 12 | // fn fmt(&self, f: &mut Formatter) -> fmt::Result { 13 | // write!( 14 | // f, 15 | // "{}°{}′{}″{}‴", 16 | // self.hour, self.minute, self.second, self.third 17 | // ) 18 | // } 19 | // } 20 | 21 | impl From for Degree { 22 | fn from(sat: Sat) -> Self { 23 | let height = sat.height().n(); 24 | Degree { 25 | hour: height / (CYCLE_EPOCHS * SUBSIDY_HALVING_INTERVAL), 26 | minute: height % SUBSIDY_HALVING_INTERVAL, 27 | second: height % DIFFCHANGE_INTERVAL, 28 | third: sat.third(), 29 | } 30 | } 31 | } 32 | 33 | // #[cfg(test)] 34 | // mod tests { 35 | // use super::*; 36 | 37 | // fn case(sat: u64, hour: u32, minute: u32, second: u32, third: u64) { 38 | // assert_eq!( 39 | // Degree::from(Sat(sat)), 40 | // Degree { 41 | // hour, 42 | // minute, 43 | // second, 44 | // third, 45 | // } 46 | // ); 47 | // } 48 | 49 | // #[test] 50 | // fn from() { 51 | // case(0, 0, 0, 0, 0); 52 | // case(1, 0, 0, 0, 1); 53 | // case(5_000_000_000, 0, 1, 1, 0); 54 | // case( 55 | // 5_000_000_000 * u64::from(DIFFCHANGE_INTERVAL), 56 | // 0, 57 | // DIFFCHANGE_INTERVAL, 58 | // 0, 59 | // 0, 60 | // ); 61 | // case( 62 | // 5_000_000_000 * u64::from(SUBSIDY_HALVING_INTERVAL), 63 | // 0, 64 | // 0, 65 | // 336, 66 | // 0, 67 | // ); 68 | // case( 69 | // (5_000_000_000 + 2_500_000_000 + 1_250_000_000 + 625_000_000 + 312_500_000 + 156_250_000) 70 | // * u64::from(SUBSIDY_HALVING_INTERVAL), 71 | // 1, 72 | // 0, 73 | // 0, 74 | // 0, 75 | // ); 76 | // } 77 | // } 78 | -------------------------------------------------------------------------------- /components/ord/src/height.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Add; 2 | 3 | use super::{epoch::Epoch, sat::Sat, *}; 4 | 5 | #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] 6 | pub struct Height(pub u32); 7 | 8 | impl Height { 9 | pub fn n(self) -> u32 { 10 | self.0 11 | } 12 | 13 | pub fn subsidy(self) -> u64 { 14 | Epoch::from(self).subsidy() 15 | } 16 | 17 | pub fn starting_sat(self) -> Sat { 18 | let epoch = Epoch::from(self); 19 | let epoch_starting_sat = epoch.starting_sat(); 20 | let epoch_starting_height = epoch.starting_height(); 21 | epoch_starting_sat + u64::from(self.n() - epoch_starting_height.n()) * epoch.subsidy() 22 | } 23 | 24 | pub fn period_offset(self) -> u32 { 25 | self.0 % DIFFCHANGE_INTERVAL 26 | } 27 | } 28 | 29 | impl Add for Height { 30 | type Output = Self; 31 | 32 | fn add(self, other: u32) -> Height { 33 | Self(self.0 + other) 34 | } 35 | } 36 | 37 | // impl Sub for Height { 38 | // type Output = Self; 39 | 40 | // fn sub(self, other: u32) -> Height { 41 | // Self(self.0 - other) 42 | // } 43 | // } 44 | 45 | // impl PartialEq for Height { 46 | // fn eq(&self, other: &u32) -> bool { 47 | // self.0 == *other 48 | // } 49 | // } 50 | 51 | // #[cfg(test)] 52 | // mod tests { 53 | // use super::*; 54 | 55 | // #[test] 56 | // fn n() { 57 | // assert_eq!(Height(0).n(), 0); 58 | // assert_eq!(Height(1).n(), 1); 59 | // } 60 | 61 | // #[test] 62 | // fn add() { 63 | // assert_eq!(Height(0) + 1, 1); 64 | // assert_eq!(Height(1) + 100, 101); 65 | // } 66 | 67 | // #[test] 68 | // fn sub() { 69 | // assert_eq!(Height(1) - 1, 0); 70 | // assert_eq!(Height(100) - 50, 50); 71 | // } 72 | 73 | // #[test] 74 | // fn eq() { 75 | // assert_eq!(Height(0), 0); 76 | // assert_eq!(Height(100), 100); 77 | // } 78 | 79 | // #[test] 80 | // fn from_str() { 81 | // assert_eq!("0".parse::().unwrap(), 0); 82 | // assert!("foo".parse::().is_err()); 83 | // } 84 | 85 | // #[test] 86 | // fn subsidy() { 87 | // assert_eq!(Height(0).subsidy(), 5000000000); 88 | // assert_eq!(Height(1).subsidy(), 5000000000); 89 | // assert_eq!(Height(SUBSIDY_HALVING_INTERVAL - 1).subsidy(), 5000000000); 90 | // assert_eq!(Height(SUBSIDY_HALVING_INTERVAL).subsidy(), 2500000000); 91 | // assert_eq!(Height(SUBSIDY_HALVING_INTERVAL + 1).subsidy(), 2500000000); 92 | // } 93 | 94 | // #[test] 95 | // fn starting_sat() { 96 | // assert_eq!(Height(0).starting_sat(), 0); 97 | // assert_eq!(Height(1).starting_sat(), 5000000000); 98 | // assert_eq!( 99 | // Height(SUBSIDY_HALVING_INTERVAL - 1).starting_sat(), 100 | // (u64::from(SUBSIDY_HALVING_INTERVAL) - 1) * 5000000000 101 | // ); 102 | // assert_eq!( 103 | // Height(SUBSIDY_HALVING_INTERVAL).starting_sat(), 104 | // u64::from(SUBSIDY_HALVING_INTERVAL) * 5000000000 105 | // ); 106 | // assert_eq!( 107 | // Height(SUBSIDY_HALVING_INTERVAL + 1).starting_sat(), 108 | // u64::from(SUBSIDY_HALVING_INTERVAL) * 5000000000 + 2500000000 109 | // ); 110 | // assert_eq!( 111 | // Height(u32::MAX).starting_sat(), 112 | // *Epoch::STARTING_SATS.last().unwrap() 113 | // ); 114 | // } 115 | 116 | // #[test] 117 | // fn period_offset() { 118 | // assert_eq!(Height(0).period_offset(), 0); 119 | // assert_eq!(Height(1).period_offset(), 1); 120 | // assert_eq!(Height(DIFFCHANGE_INTERVAL - 1).period_offset(), 2015); 121 | // assert_eq!(Height(DIFFCHANGE_INTERVAL).period_offset(), 0); 122 | // assert_eq!(Height(DIFFCHANGE_INTERVAL + 1).period_offset(), 1); 123 | // } 124 | // } 125 | -------------------------------------------------------------------------------- /components/ord/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_variables)] 3 | 4 | #[macro_use] 5 | extern crate serde_derive; 6 | 7 | type Result = std::result::Result; 8 | 9 | pub mod chain; 10 | pub mod charm; 11 | pub mod decimal_sat; 12 | pub mod degree; 13 | pub mod envelope; 14 | pub mod epoch; 15 | pub mod height; 16 | pub mod inscription; 17 | pub mod inscription_id; 18 | pub mod media; 19 | pub mod rarity; 20 | pub mod sat; 21 | pub mod sat_point; 22 | pub mod tag; 23 | 24 | pub const SUBSIDY_HALVING_INTERVAL: u32 = 210_000; 25 | pub const DIFFCHANGE_INTERVAL: u32 = 2016; 26 | pub const CYCLE_EPOCHS: u32 = 6; 27 | pub const COIN_VALUE: u64 = 100_000_000; 28 | -------------------------------------------------------------------------------- /components/ord/src/tag.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, mem}; 2 | 3 | use bitcoin::{constants::MAX_SCRIPT_ELEMENT_SIZE, script}; 4 | 5 | #[derive(Copy, Clone)] 6 | #[repr(u8)] 7 | pub(crate) enum Tag { 8 | Pointer = 2, 9 | #[allow(unused)] 10 | Unbound = 66, 11 | 12 | ContentType = 1, 13 | Parent = 3, 14 | Metadata = 5, 15 | Metaprotocol = 7, 16 | ContentEncoding = 9, 17 | Delegate = 11, 18 | Rune = 13, 19 | #[allow(unused)] 20 | Note = 15, 21 | #[allow(unused)] 22 | Nop = 255, 23 | } 24 | 25 | impl Tag { 26 | fn chunked(self) -> bool { 27 | matches!(self, Self::Metadata) 28 | } 29 | 30 | pub(crate) fn bytes(self) -> [u8; 1] { 31 | [self as u8] 32 | } 33 | 34 | pub(crate) fn append(self, builder: &mut script::Builder, value: &Option>) { 35 | if let Some(value) = value { 36 | let mut tmp = script::Builder::new(); 37 | mem::swap(&mut tmp, builder); 38 | 39 | if self.chunked() { 40 | for chunk in value.chunks(MAX_SCRIPT_ELEMENT_SIZE) { 41 | tmp = tmp 42 | .push_slice::<&script::PushBytes>( 43 | self.bytes().as_slice().try_into().unwrap(), 44 | ) 45 | .push_slice::<&script::PushBytes>(chunk.try_into().unwrap()); 46 | } 47 | } else { 48 | tmp = tmp 49 | .push_slice::<&script::PushBytes>(self.bytes().as_slice().try_into().unwrap()) 50 | .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap()); 51 | } 52 | 53 | mem::swap(&mut tmp, builder); 54 | } 55 | } 56 | 57 | pub(crate) fn append_array(self, builder: &mut script::Builder, values: &Vec>) { 58 | let mut tmp = script::Builder::new(); 59 | mem::swap(&mut tmp, builder); 60 | 61 | for value in values { 62 | tmp = tmp 63 | .push_slice::<&script::PushBytes>(self.bytes().as_slice().try_into().unwrap()) 64 | .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap()); 65 | } 66 | 67 | mem::swap(&mut tmp, builder); 68 | } 69 | 70 | pub(crate) fn take(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Option> { 71 | if self.chunked() { 72 | let value = fields.remove(self.bytes().as_slice())?; 73 | 74 | if value.is_empty() { 75 | None 76 | } else { 77 | Some(value.into_iter().flatten().cloned().collect()) 78 | } 79 | } else { 80 | let values = fields.get_mut(self.bytes().as_slice())?; 81 | 82 | if values.is_empty() { 83 | None 84 | } else { 85 | let value = values.remove(0).to_vec(); 86 | 87 | if values.is_empty() { 88 | fields.remove(self.bytes().as_slice()); 89 | } 90 | 91 | Some(value) 92 | } 93 | } 94 | } 95 | 96 | pub(crate) fn take_array(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Vec> { 97 | fields 98 | .remove(self.bytes().as_slice()) 99 | .unwrap_or_default() 100 | .into_iter() 101 | .map(|v| v.to_vec()) 102 | .collect() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /components/ordinals/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ordinals" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | num_cpus = "1.16.0" 8 | serde = "1" 9 | serde_json = "1" 10 | serde_derive = "1" 11 | hex = "0.4.3" 12 | rand = "0.9.0" 13 | lru = "0.13.0" 14 | config = { path = "../config" } 15 | bitcoin = { workspace = true } 16 | bitcoind = { path = "../bitcoind" } 17 | hiro-system-kit = { workspace = true } 18 | reqwest = { version = "0.11", default-features = false, features = [ 19 | "stream", 20 | "json", 21 | "rustls-tls", 22 | ] } 23 | tokio = { workspace = true } 24 | futures-util = "0.3.24" 25 | flate2 = "1.0.24" 26 | tar = "0.4.38" 27 | flume = "0.11.0" 28 | ansi_term = "0.12.1" 29 | atty = "0.2.14" 30 | crossbeam-channel = "0.5.8" 31 | threadpool = "1.8.1" 32 | dashmap = "5.4.0" 33 | fxhash = "0.2.1" 34 | anyhow = { version = "1.0.56", features = ["backtrace"] } 35 | progressing = '3' 36 | futures = "0.3.28" 37 | rocksdb = { version = "0.23.0", default-features = false, features = [ 38 | "snappy", "bindgen-runtime" 39 | ] } 40 | pprof = { version = "0.14.0", features = ["flamegraph"], optional = true } 41 | hyper = { version = "=0.14.27" } 42 | lazy_static = { version = "1.4.0" } 43 | regex = "1.10.3" 44 | prometheus = { workspace = true } 45 | postgres = { path = "../postgres" } 46 | tokio-postgres = { workspace = true } 47 | deadpool-postgres = { workspace = true } 48 | refinery = { workspace = true } 49 | maplit = "1.0.2" 50 | ord = { path = "../ord" } 51 | 52 | [dev-dependencies] 53 | test-case = "3.1.0" 54 | 55 | # [profile.release] 56 | # debug = true 57 | 58 | [features] 59 | debug = ["hiro-system-kit/debug", "pprof"] 60 | release = ["hiro-system-kit/release"] 61 | -------------------------------------------------------------------------------- /components/ordinals/src/core/meta_protocols/brc20/mod.rs: -------------------------------------------------------------------------------- 1 | use bitcoind::types::BitcoinNetwork; 2 | 3 | pub mod brc20_pg; 4 | pub mod cache; 5 | pub mod index; 6 | pub mod models; 7 | pub mod parser; 8 | pub mod test_utils; 9 | pub mod verifier; 10 | 11 | pub fn brc20_activation_height(network: &BitcoinNetwork) -> u64 { 12 | match network { 13 | BitcoinNetwork::Mainnet => 779832, 14 | BitcoinNetwork::Regtest => 0, 15 | BitcoinNetwork::Testnet => 0, 16 | BitcoinNetwork::Signet => 0, 17 | } 18 | } 19 | 20 | pub fn brc20_self_mint_activation_height(network: &BitcoinNetwork) -> u64 { 21 | match network { 22 | BitcoinNetwork::Mainnet => 837090, 23 | BitcoinNetwork::Regtest => 0, 24 | BitcoinNetwork::Testnet => 0, 25 | BitcoinNetwork::Signet => 0, 26 | } 27 | } 28 | 29 | /// Transform a BRC-20 amount `String` (that may or may not have decimals) to a `u128` value we can store in Postgres. The amount 30 | /// will be shifted to the left by however many decimals the token uses. 31 | pub fn decimals_str_amount_to_u128(amt: &String, decimals: u8) -> Result { 32 | let parts: Vec<&str> = amt.split('.').collect(); 33 | let first = parts 34 | .first() 35 | .ok_or("decimals_str_amount_to_u128: first part not found")?; 36 | let integer = (*first) 37 | .parse::() 38 | .map_err(|e| format!("decimals_str_amount_to_u128: {e}"))?; 39 | 40 | let mut fractional = 0u128; 41 | if let Some(second) = parts.get(1) { 42 | let mut padded = String::with_capacity(decimals as usize); 43 | padded.push_str(second); 44 | padded.push_str(&"0".repeat(decimals as usize - (*second).len())); 45 | fractional = padded 46 | .parse::() 47 | .map_err(|e| format!("decimals_str_amount_to_u128: {e}"))?; 48 | }; 49 | 50 | Ok((integer * 10u128.pow(decimals as u32)) + fractional) 51 | } 52 | 53 | /// Transform a BRC-20 amount which was stored in Postgres as a `u128` back to a `String` with decimals included. 54 | pub fn u128_amount_to_decimals_str(amount: u128, decimals: u8) -> String { 55 | let num_str = amount.to_string(); 56 | if decimals == 0 { 57 | return num_str; 58 | } 59 | let decimal_point = num_str.len() as i32 - decimals as i32; 60 | if decimal_point < 0 { 61 | let padding = "0".repeat(decimal_point.unsigned_abs() as usize); 62 | format!("0.{padding}{num_str}") 63 | } else { 64 | let (integer, fractional) = num_str.split_at(decimal_point as usize); 65 | format!("{}.{}", integer, fractional) 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod test { 71 | use test_case::test_case; 72 | 73 | use super::{decimals_str_amount_to_u128, u128_amount_to_decimals_str}; 74 | 75 | #[test_case((1000000000000000000, 18) => "1.000000000000000000".to_string(); "with whole number")] 76 | #[test_case((80000000000000000, 18) => "0.080000000000000000".to_string(); "with decimal number")] 77 | fn test_u128_to_decimals_str((amount, decimals): (u128, u8)) -> String { 78 | u128_amount_to_decimals_str(amount, decimals) 79 | } 80 | 81 | #[test_case((&"1.000000000000000000".to_string(), 18) => 1000000000000000000; "with whole number")] 82 | #[test_case((&"1".to_string(), 18) => 1000000000000000000; "with whole number no decimals")] 83 | #[test_case((&"0.080000000000000000".to_string(), 18) => 80000000000000000; "with decimal number")] 84 | fn test_decimals_str_to_u128((amount, decimals): (&String, u8)) -> u128 { 85 | decimals_str_amount_to_u128(amount, decimals).unwrap() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /components/ordinals/src/core/meta_protocols/brc20/models/db_operation.rs: -------------------------------------------------------------------------------- 1 | use postgres::{ 2 | types::{PgBigIntU32, PgNumericU128, PgNumericU64}, 3 | FromPgRow, 4 | }; 5 | use tokio_postgres::Row; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct DbOperation { 9 | pub ticker: String, 10 | pub operation: String, 11 | pub inscription_id: String, 12 | pub inscription_number: i64, 13 | pub ordinal_number: PgNumericU64, 14 | pub block_height: PgNumericU64, 15 | pub block_hash: String, 16 | pub tx_id: String, 17 | pub tx_index: PgNumericU64, 18 | pub output: String, 19 | pub offset: PgNumericU64, 20 | pub timestamp: PgBigIntU32, 21 | pub address: String, 22 | pub to_address: Option, 23 | pub amount: PgNumericU128, 24 | } 25 | 26 | impl FromPgRow for DbOperation { 27 | fn from_pg_row(row: &Row) -> Self { 28 | DbOperation { 29 | ticker: row.get("ticker"), 30 | operation: row.get("operation"), 31 | inscription_id: row.get("inscription_id"), 32 | inscription_number: row.get("inscription_number"), 33 | ordinal_number: row.get("ordinal_number"), 34 | block_height: row.get("block_height"), 35 | block_hash: row.get("block_hash"), 36 | tx_id: row.get("tx_id"), 37 | tx_index: row.get("tx_index"), 38 | output: row.get("output"), 39 | offset: row.get("offset"), 40 | timestamp: row.get("timestamp"), 41 | address: row.get("address"), 42 | to_address: row.get("to_address"), 43 | amount: row.get("amount"), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /components/ordinals/src/core/meta_protocols/brc20/models/db_token.rs: -------------------------------------------------------------------------------- 1 | use postgres::{ 2 | types::{PgBigIntU32, PgNumericU128, PgNumericU64, PgSmallIntU8}, 3 | FromPgRow, 4 | }; 5 | use tokio_postgres::Row; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq)] 8 | pub struct DbToken { 9 | pub ticker: String, 10 | pub display_ticker: String, 11 | pub inscription_id: String, 12 | pub inscription_number: i64, 13 | pub block_height: PgNumericU64, 14 | pub block_hash: String, 15 | pub tx_id: String, 16 | pub tx_index: PgNumericU64, 17 | pub address: String, 18 | pub max: PgNumericU128, 19 | pub limit: PgNumericU128, 20 | pub decimals: PgSmallIntU8, 21 | pub self_mint: bool, 22 | pub minted_supply: PgNumericU128, 23 | pub tx_count: i32, 24 | pub timestamp: PgBigIntU32, 25 | } 26 | 27 | impl FromPgRow for DbToken { 28 | fn from_pg_row(row: &Row) -> Self { 29 | DbToken { 30 | ticker: row.get("ticker"), 31 | display_ticker: row.get("display_ticker"), 32 | inscription_id: row.get("inscription_id"), 33 | inscription_number: row.get("inscription_number"), 34 | block_height: row.get("block_height"), 35 | block_hash: row.get("block_hash"), 36 | tx_id: row.get("tx_id"), 37 | tx_index: row.get("tx_index"), 38 | address: row.get("address"), 39 | max: row.get("max"), 40 | limit: row.get("limit"), 41 | decimals: row.get("decimals"), 42 | self_mint: row.get("self_mint"), 43 | minted_supply: row.get("minted_supply"), 44 | tx_count: row.get("tx_count"), 45 | timestamp: row.get("timestamp"), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/ordinals/src/core/meta_protocols/brc20/models/mod.rs: -------------------------------------------------------------------------------- 1 | mod db_operation; 2 | mod db_token; 3 | 4 | pub use db_operation::DbOperation; 5 | pub use db_token::DbToken; 6 | -------------------------------------------------------------------------------- /components/ordinals/src/core/meta_protocols/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod brc20; 2 | -------------------------------------------------------------------------------- /components/ordinals/src/core/pipeline/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod processors; 2 | -------------------------------------------------------------------------------- /components/ordinals/src/core/pipeline/processors/block_archiving.rs: -------------------------------------------------------------------------------- 1 | use bitcoind::{try_error, try_info, utils::Context}; 2 | use rocksdb::DB; 3 | 4 | use crate::db::blocks::insert_entry_in_blocks; 5 | 6 | pub fn store_compacted_blocks( 7 | mut compacted_blocks: Vec<(u64, Vec)>, 8 | update_tip: bool, 9 | blocks_db_rw: &DB, 10 | ctx: &Context, 11 | ) { 12 | compacted_blocks.sort_by(|(a, _), (b, _)| a.cmp(b)); 13 | 14 | for (block_height, compacted_block) in compacted_blocks.into_iter() { 15 | insert_entry_in_blocks( 16 | block_height as u32, 17 | &compacted_block, 18 | update_tip, 19 | blocks_db_rw, 20 | ctx, 21 | ); 22 | try_info!(ctx, "Compacted block #{block_height} saved to disk"); 23 | } 24 | 25 | if let Err(e) = blocks_db_rw.flush() { 26 | try_error!(ctx, "{}", e.to_string()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /components/ordinals/src/core/pipeline/processors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block_archiving; 2 | pub mod inscription_indexing; 3 | -------------------------------------------------------------------------------- /components/ordinals/src/core/protocol/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod inscription_parsing; 2 | pub mod inscription_sequencing; 3 | pub mod satoshi_numbering; 4 | pub mod satoshi_tracking; 5 | pub mod sequence_cursor; 6 | -------------------------------------------------------------------------------- /components/ordinals/src/db/models/db_current_location.rs: -------------------------------------------------------------------------------- 1 | use bitcoind::types::{ 2 | BlockIdentifier, OrdinalInscriptionRevealData, OrdinalInscriptionTransferData, 3 | OrdinalInscriptionTransferDestination, TransactionIdentifier, 4 | }; 5 | use postgres::{ 6 | types::{PgBigIntU32, PgNumericU64}, 7 | FromPgRow, 8 | }; 9 | use tokio_postgres::Row; 10 | 11 | use crate::core::protocol::satoshi_tracking::parse_output_and_offset_from_satpoint; 12 | 13 | #[derive(Debug, Clone, PartialEq, Eq)] 14 | pub struct DbCurrentLocation { 15 | pub ordinal_number: PgNumericU64, 16 | pub block_height: PgNumericU64, 17 | pub tx_id: String, 18 | pub tx_index: PgBigIntU32, 19 | pub address: Option, 20 | pub output: String, 21 | pub offset: Option, 22 | } 23 | 24 | impl DbCurrentLocation { 25 | pub fn from_reveal( 26 | reveal: &OrdinalInscriptionRevealData, 27 | block_identifier: &BlockIdentifier, 28 | tx_identifier: &TransactionIdentifier, 29 | tx_index: usize, 30 | ) -> Self { 31 | let (output, offset) = 32 | parse_output_and_offset_from_satpoint(&reveal.satpoint_post_inscription).unwrap(); 33 | DbCurrentLocation { 34 | ordinal_number: PgNumericU64(reveal.ordinal_number), 35 | block_height: PgNumericU64(block_identifier.index), 36 | tx_id: tx_identifier.hash[2..].to_string(), 37 | tx_index: PgBigIntU32(tx_index as u32), 38 | address: reveal.inscriber_address.clone(), 39 | output, 40 | offset: offset.map(PgNumericU64), 41 | } 42 | } 43 | 44 | pub fn from_transfer( 45 | transfer: &OrdinalInscriptionTransferData, 46 | block_identifier: &BlockIdentifier, 47 | tx_identifier: &TransactionIdentifier, 48 | tx_index: usize, 49 | ) -> Self { 50 | let (output, offset) = 51 | parse_output_and_offset_from_satpoint(&transfer.satpoint_post_transfer).unwrap(); 52 | DbCurrentLocation { 53 | ordinal_number: PgNumericU64(transfer.ordinal_number), 54 | block_height: PgNumericU64(block_identifier.index), 55 | tx_id: tx_identifier.hash[2..].to_string(), 56 | tx_index: PgBigIntU32(tx_index as u32), 57 | address: match &transfer.destination { 58 | OrdinalInscriptionTransferDestination::Transferred(address) => { 59 | Some(address.clone()) 60 | } 61 | OrdinalInscriptionTransferDestination::SpentInFees => None, 62 | OrdinalInscriptionTransferDestination::Burnt(_) => None, 63 | }, 64 | output, 65 | offset: offset.map(PgNumericU64), 66 | } 67 | } 68 | } 69 | 70 | impl FromPgRow for DbCurrentLocation { 71 | fn from_pg_row(row: &Row) -> Self { 72 | DbCurrentLocation { 73 | ordinal_number: row.get("ordinal_number"), 74 | block_height: row.get("block_height"), 75 | tx_id: row.get("tx_id"), 76 | tx_index: row.get("tx_index"), 77 | address: row.get("address"), 78 | output: row.get("output"), 79 | offset: row.get("offset"), 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /components/ordinals/src/db/models/db_inscription_parent.rs: -------------------------------------------------------------------------------- 1 | use bitcoind::types::OrdinalInscriptionRevealData; 2 | 3 | #[derive(Debug, Clone, PartialEq, Eq)] 4 | pub struct DbInscriptionParent { 5 | pub inscription_id: String, 6 | pub parent_inscription_id: String, 7 | } 8 | 9 | impl DbInscriptionParent { 10 | pub fn from_reveal(reveal: &OrdinalInscriptionRevealData) -> Result, String> { 11 | Ok(reveal 12 | .parents 13 | .iter() 14 | .map(|p| DbInscriptionParent { 15 | inscription_id: reveal.inscription_id.clone(), 16 | parent_inscription_id: p.clone(), 17 | }) 18 | .collect()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/ordinals/src/db/models/db_satoshi.rs: -------------------------------------------------------------------------------- 1 | use bitcoind::types::OrdinalInscriptionRevealData; 2 | use ord::{rarity::Rarity, sat::Sat}; 3 | use postgres::{types::PgNumericU64, FromPgRow}; 4 | use tokio_postgres::Row; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub struct DbSatoshi { 8 | pub ordinal_number: PgNumericU64, 9 | pub rarity: String, 10 | pub coinbase_height: PgNumericU64, 11 | } 12 | 13 | impl DbSatoshi { 14 | pub fn from_reveal(reveal: &OrdinalInscriptionRevealData) -> Self { 15 | let rarity = Rarity::from(Sat(reveal.ordinal_number)); 16 | DbSatoshi { 17 | ordinal_number: PgNumericU64(reveal.ordinal_number), 18 | rarity: rarity.to_string(), 19 | coinbase_height: PgNumericU64(reveal.ordinal_block_height), 20 | } 21 | } 22 | } 23 | 24 | impl FromPgRow for DbSatoshi { 25 | fn from_pg_row(row: &Row) -> Self { 26 | DbSatoshi { 27 | ordinal_number: row.get("ordinal_number"), 28 | rarity: row.get("rarity"), 29 | coinbase_height: row.get("coinbase_height"), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/ordinals/src/db/models/mod.rs: -------------------------------------------------------------------------------- 1 | mod db_current_location; 2 | mod db_inscription; 3 | mod db_inscription_parent; 4 | mod db_inscription_recursion; 5 | mod db_location; 6 | mod db_satoshi; 7 | 8 | pub use db_current_location::DbCurrentLocation; 9 | pub use db_inscription::DbInscription; 10 | pub use db_inscription_parent::DbInscriptionParent; 11 | pub use db_inscription_recursion::DbInscriptionRecursion; 12 | pub use db_location::DbLocation; 13 | pub use db_satoshi::DbSatoshi; 14 | -------------------------------------------------------------------------------- /components/ordinals/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod monitoring; 2 | 3 | use bitcoind::types::TransactionIdentifier; 4 | 5 | pub fn format_inscription_id( 6 | transaction_identifier: &TransactionIdentifier, 7 | inscription_subindex: usize, 8 | ) -> String { 9 | format!( 10 | "{}i{}", 11 | transaction_identifier.get_hash_bytes_str(), 12 | inscription_subindex, 13 | ) 14 | } 15 | 16 | pub fn format_outpoint_to_watch( 17 | transaction_identifier: &TransactionIdentifier, 18 | output_index: usize, 19 | ) -> String { 20 | format!( 21 | "{}:{}", 22 | transaction_identifier.get_hash_bytes_str(), 23 | output_index 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /components/postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "postgres" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | config = { path = "../config" } 8 | bytes = "1.3" 9 | deadpool-postgres = { workspace = true } 10 | num-traits = "0.2.14" 11 | slog = { version = "2.7.0" } 12 | tokio = { workspace = true } 13 | tokio-postgres = { workspace = true } 14 | 15 | [dev-dependencies] 16 | test-case = "3.1.0" 17 | -------------------------------------------------------------------------------- /components/postgres/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | mod pg_bigint_u32; 2 | mod pg_numeric_u128; 3 | mod pg_numeric_u64; 4 | mod pg_smallint_u8; 5 | 6 | pub use pg_bigint_u32::PgBigIntU32; 7 | pub use pg_numeric_u128::PgNumericU128; 8 | pub use pg_numeric_u64::PgNumericU64; 9 | pub use pg_smallint_u8::PgSmallIntU8; 10 | -------------------------------------------------------------------------------- /components/postgres/src/types/pg_bigint_u32.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, error::Error, ops::AddAssign}; 2 | 3 | use bytes::{BufMut, BytesMut}; 4 | use tokio_postgres::types::{to_sql_checked, FromSql, IsNull, ToSql, Type}; 5 | 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 7 | pub struct PgBigIntU32(pub u32); 8 | 9 | impl ToSql for PgBigIntU32 { 10 | fn to_sql( 11 | &self, 12 | _ty: &Type, 13 | out: &mut BytesMut, 14 | ) -> Result> { 15 | out.put_u64(self.0 as u64); 16 | Ok(IsNull::No) 17 | } 18 | 19 | fn accepts(ty: &Type) -> bool { 20 | ty.name() == "int8" || ty.name() == "bigint" 21 | } 22 | 23 | to_sql_checked!(); 24 | } 25 | 26 | impl<'a> FromSql<'a> for PgBigIntU32 { 27 | fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result> { 28 | let mut arr = [0u8; 4]; 29 | arr.copy_from_slice(&raw[4..8]); 30 | Ok(PgBigIntU32(u32::from_be_bytes(arr))) 31 | } 32 | 33 | fn accepts(ty: &Type) -> bool { 34 | ty.name() == "int8" || ty.name() == "bigint" 35 | } 36 | } 37 | 38 | impl AddAssign for PgBigIntU32 { 39 | fn add_assign(&mut self, other: u32) { 40 | self.0 += other; 41 | } 42 | } 43 | 44 | impl PartialOrd for PgBigIntU32 { 45 | fn partial_cmp(&self, other: &Self) -> Option { 46 | Some(self.0.cmp(&other.0)) 47 | } 48 | } 49 | 50 | impl Ord for PgBigIntU32 { 51 | fn cmp(&self, other: &Self) -> Ordering { 52 | self.0.cmp(&other.0) 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod test { 58 | use test_case::test_case; 59 | 60 | use super::PgBigIntU32; 61 | use crate::pg_test_client; 62 | 63 | #[test_case(4294967295; "u32 max")] 64 | #[test_case(0; "zero")] 65 | #[tokio::test] 66 | async fn test_u32_to_postgres(val: u32) { 67 | let mut client = pg_test_client().await; 68 | let value = PgBigIntU32(val); 69 | let tx = client.transaction().await.unwrap(); 70 | let _ = tx.query("CREATE TABLE test (value BIGINT)", &[]).await; 71 | let _ = tx 72 | .query("INSERT INTO test (value) VALUES ($1)", &[&value]) 73 | .await; 74 | let row = tx.query_one("SELECT value FROM test", &[]).await.unwrap(); 75 | let res: PgBigIntU32 = row.get("value"); 76 | let _ = tx.rollback().await; 77 | assert_eq!(res.0, value.0); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /components/postgres/src/types/pg_numeric_u64.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, error::Error}; 2 | 3 | use bytes::BytesMut; 4 | use num_traits::ToPrimitive; 5 | use tokio_postgres::types::{to_sql_checked, FromSql, IsNull, ToSql, Type}; 6 | 7 | use super::pg_numeric_u128::{pg_numeric_bytes_to_u128, u128_into_pg_numeric_bytes}; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 10 | pub struct PgNumericU64(pub u64); 11 | 12 | impl ToSql for PgNumericU64 { 13 | fn to_sql( 14 | &self, 15 | _ty: &Type, 16 | out: &mut BytesMut, 17 | ) -> Result> { 18 | u128_into_pg_numeric_bytes(self.0 as u128, out); 19 | Ok(IsNull::No) 20 | } 21 | 22 | fn accepts(ty: &Type) -> bool { 23 | ty.name() == "numeric" 24 | } 25 | 26 | to_sql_checked!(); 27 | } 28 | 29 | impl<'a> FromSql<'a> for PgNumericU64 { 30 | fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result> { 31 | let result = pg_numeric_bytes_to_u128(raw); 32 | Ok(PgNumericU64(result.to_u64().unwrap())) 33 | } 34 | 35 | fn accepts(ty: &Type) -> bool { 36 | ty.name() == "numeric" 37 | } 38 | } 39 | 40 | impl PartialOrd for PgNumericU64 { 41 | fn partial_cmp(&self, other: &Self) -> Option { 42 | Some(self.0.cmp(&other.0)) 43 | } 44 | } 45 | 46 | impl Ord for PgNumericU64 { 47 | fn cmp(&self, other: &Self) -> Ordering { 48 | self.0.cmp(&other.0) 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod test { 54 | use test_case::test_case; 55 | 56 | use super::PgNumericU64; 57 | use crate::pg_test_client; 58 | 59 | #[test_case(18446744073709551615; "u64 max")] 60 | #[test_case(800000000000; "with trailing zeros")] 61 | #[test_case(0; "zero")] 62 | #[tokio::test] 63 | async fn test_u64_to_postgres(val: u64) { 64 | let mut client = pg_test_client().await; 65 | let value = PgNumericU64(val); 66 | let tx = client.transaction().await.unwrap(); 67 | let _ = tx.query("CREATE TABLE test (value NUMERIC)", &[]).await; 68 | let _ = tx 69 | .query("INSERT INTO test (value) VALUES ($1)", &[&value]) 70 | .await; 71 | let row = tx.query_one("SELECT value FROM test", &[]).await.unwrap(); 72 | let res: PgNumericU64 = row.get("value"); 73 | let _ = tx.rollback().await; 74 | assert_eq!(res.0, value.0); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /components/postgres/src/types/pg_smallint_u8.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use bytes::{BufMut, BytesMut}; 4 | use tokio_postgres::types::{to_sql_checked, FromSql, IsNull, ToSql, Type}; 5 | 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 7 | pub struct PgSmallIntU8(pub u8); 8 | 9 | impl ToSql for PgSmallIntU8 { 10 | fn to_sql( 11 | &self, 12 | _ty: &Type, 13 | out: &mut BytesMut, 14 | ) -> Result> { 15 | out.put_u16(self.0 as u16); 16 | Ok(IsNull::No) 17 | } 18 | 19 | fn accepts(ty: &Type) -> bool { 20 | ty.name() == "int2" || ty.name() == "smallint" 21 | } 22 | 23 | to_sql_checked!(); 24 | } 25 | 26 | impl<'a> FromSql<'a> for PgSmallIntU8 { 27 | fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result> { 28 | let mut arr = [0u8; 1]; 29 | arr.copy_from_slice(&raw[1..2]); 30 | Ok(PgSmallIntU8(u8::from_be_bytes(arr))) 31 | } 32 | 33 | fn accepts(ty: &Type) -> bool { 34 | ty.name() == "int2" || ty.name() == "smallint" 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod test { 40 | use test_case::test_case; 41 | 42 | use super::PgSmallIntU8; 43 | use crate::pg_test_client; 44 | 45 | #[test_case(255; "u8 max")] 46 | #[test_case(0; "zero")] 47 | #[tokio::test] 48 | async fn test_u8_to_postgres(val: u8) { 49 | let mut client = pg_test_client().await; 50 | let value = PgSmallIntU8(val); 51 | let tx = client.transaction().await.unwrap(); 52 | let _ = tx.query("CREATE TABLE test (value SMALLINT)", &[]).await; 53 | let _ = tx 54 | .query("INSERT INTO test (value) VALUES ($1)", &[&value]) 55 | .await; 56 | let row = tx.query_one("SELECT value FROM test", &[]).await.unwrap(); 57 | let res: PgSmallIntU8 = row.get("value"); 58 | let _ = tx.rollback().await; 59 | assert_eq!(res.0, value.0); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /components/postgres/src/utils.rs: -------------------------------------------------------------------------------- 1 | /// Returns a String query fragment useful for mass insertions of data in a single query. 2 | /// For example, rows = 2 and columns = 3 returns "(($1, $2, $3), ($4, $5, $6))" 3 | pub fn multi_row_query_param_str(rows: usize, columns: usize) -> String { 4 | let mut arg_num = 1; 5 | let mut arg_str = String::new(); 6 | for _ in 0..rows { 7 | arg_str.push('('); 8 | for i in 0..columns { 9 | arg_str.push_str(format!("${},", arg_num + i).as_str()); 10 | } 11 | arg_str.pop(); 12 | arg_str.push_str("),"); 13 | arg_num += columns; 14 | } 15 | arg_str.pop(); 16 | arg_str 17 | } 18 | -------------------------------------------------------------------------------- /components/runes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "runes" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | bitcoind = { path = "../bitcoind" } 8 | bitcoin = { workspace = true } 9 | lru = "0.12.3" 10 | ordinals-parser = { package = "ordinals", version = "0.0.15" } 11 | bytes = "1.3" 12 | config = { path = "../config" } 13 | serde = "1" 14 | serde_derive = "1" 15 | hex = "0.4.3" 16 | rand = "0.8.5" 17 | hiro-system-kit = { workspace = true } 18 | ctrlc = { version = "3.2.2", optional = true } 19 | crossbeam-channel = "0.5.8" 20 | clap = { version = "4.3.2", features = ["derive"] } 21 | clap_generate = { version = "3.0.3" } 22 | postgres = { path = "../postgres" } 23 | tokio = { workspace = true } 24 | tokio-postgres = { workspace = true } 25 | deadpool-postgres = { workspace = true } 26 | refinery = { workspace = true } 27 | num-traits = "0.2.14" 28 | maplit = "1.0.2" 29 | prometheus = { workspace = true } 30 | hyper = { version = "0.14", features = ["full"] } 31 | 32 | [dev-dependencies] 33 | test-case = "3.1.0" 34 | 35 | [features] 36 | debug = ["hiro-system-kit/debug"] 37 | release = ["hiro-system-kit/release"] 38 | -------------------------------------------------------------------------------- /components/runes/src/db/cache/db_cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use bitcoind::{try_debug, try_info, utils::Context}; 4 | use tokio_postgres::Transaction; 5 | 6 | use crate::db::{ 7 | models::{ 8 | db_balance_change::DbBalanceChange, db_ledger_entry::DbLedgerEntry, db_rune::DbRune, 9 | db_supply_change::DbSupplyChange, 10 | }, 11 | pg_insert_balance_changes, pg_insert_ledger_entries, pg_insert_runes, pg_insert_supply_changes, 12 | }; 13 | 14 | /// Holds rows that have yet to be inserted into the database. 15 | pub struct DbCache { 16 | pub runes: Vec, 17 | pub ledger_entries: Vec, 18 | pub supply_changes: HashMap, 19 | pub balance_increases: HashMap<(String, String), DbBalanceChange>, 20 | pub balance_deductions: HashMap<(String, String), DbBalanceChange>, 21 | } 22 | 23 | impl Default for DbCache { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl DbCache { 30 | pub fn new() -> Self { 31 | DbCache { 32 | runes: Vec::new(), 33 | ledger_entries: Vec::new(), 34 | supply_changes: HashMap::new(), 35 | balance_increases: HashMap::new(), 36 | balance_deductions: HashMap::new(), 37 | } 38 | } 39 | 40 | /// Insert all data into the DB and clear cache. 41 | pub async fn flush(&mut self, db_tx: &mut Transaction<'_>, ctx: &Context) { 42 | try_info!(ctx, "Flushing DB cache..."); 43 | if !self.runes.is_empty() { 44 | try_debug!(ctx, "Flushing {} runes", self.runes.len()); 45 | let _ = pg_insert_runes(&self.runes, db_tx, ctx).await; 46 | self.runes.clear(); 47 | } 48 | if !self.supply_changes.is_empty() { 49 | try_debug!(ctx, "Flushing {} supply changes", self.supply_changes.len()); 50 | let _ = pg_insert_supply_changes( 51 | &self.supply_changes.values().cloned().collect::>(), 52 | db_tx, 53 | ctx, 54 | ) 55 | .await; 56 | self.supply_changes.clear(); 57 | } 58 | if !self.ledger_entries.is_empty() { 59 | try_debug!(ctx, "Flushing {} ledger entries", self.ledger_entries.len()); 60 | let _ = pg_insert_ledger_entries(&self.ledger_entries, db_tx, ctx).await; 61 | self.ledger_entries.clear(); 62 | } 63 | if !self.balance_increases.is_empty() { 64 | try_debug!( 65 | ctx, 66 | "Flushing {} balance increases", 67 | self.balance_increases.len() 68 | ); 69 | let _ = pg_insert_balance_changes( 70 | &self.balance_increases.values().cloned().collect::>(), 71 | true, 72 | db_tx, 73 | ctx, 74 | ) 75 | .await; 76 | self.balance_increases.clear(); 77 | } 78 | if !self.balance_deductions.is_empty() { 79 | try_debug!( 80 | ctx, 81 | "Flushing {} balance deductions", 82 | self.balance_deductions.len() 83 | ); 84 | let _ = pg_insert_balance_changes( 85 | &self 86 | .balance_deductions 87 | .values() 88 | .cloned() 89 | .collect::>(), 90 | false, 91 | db_tx, 92 | ctx, 93 | ) 94 | .await; 95 | self.balance_deductions.clear(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /components/runes/src/db/cache/input_rune_balance.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct InputRuneBalance { 3 | /// Previous owner of this balance. If this is `None`, it means the balance was just minted or premined. 4 | pub address: Option, 5 | /// How much balance was input to this transaction. 6 | pub amount: u128, 7 | } 8 | 9 | #[cfg(test)] 10 | impl InputRuneBalance { 11 | pub fn dummy() -> Self { 12 | InputRuneBalance { 13 | address: Some( 14 | "bc1p8zxlhgdsq6dmkzk4ammzcx55c3hfrg69ftx0gzlnfwq0wh38prds0nzqwf".to_string(), 15 | ), 16 | amount: 1000, 17 | } 18 | } 19 | 20 | pub fn amount(&mut self, amount: u128) -> &mut Self { 21 | self.amount = amount; 22 | return self; 23 | } 24 | 25 | pub fn address(&mut self, address: Option) -> &mut Self { 26 | self.address = address; 27 | return self; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/runes/src/db/cache/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db_cache; 2 | pub mod index_cache; 3 | pub mod input_rune_balance; 4 | pub mod transaction_cache; 5 | pub mod transaction_location; 6 | pub mod utils; 7 | -------------------------------------------------------------------------------- /components/runes/src/db/cache/transaction_location.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bitcoin::Network; 4 | use ordinals_parser::RuneId; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct TransactionLocation { 8 | pub network: Network, 9 | pub block_hash: String, 10 | pub block_height: u64, 11 | pub timestamp: u32, 12 | pub tx_index: u32, 13 | pub tx_id: String, 14 | } 15 | 16 | impl TransactionLocation { 17 | pub fn rune_id(&self) -> RuneId { 18 | RuneId { 19 | block: self.block_height, 20 | tx: self.tx_index, 21 | } 22 | } 23 | } 24 | 25 | impl fmt::Display for TransactionLocation { 26 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 27 | write!( 28 | f, 29 | "tx: {} ({}) @{}", 30 | self.tx_id, self.tx_index, self.block_height 31 | ) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | impl TransactionLocation { 37 | pub fn dummy() -> Self { 38 | TransactionLocation { 39 | network: Network::Bitcoin, 40 | block_hash: "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" 41 | .to_string(), 42 | block_height: 840000, 43 | timestamp: 1713571767, 44 | tx_index: 0, 45 | tx_id: "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e".to_string(), 46 | } 47 | } 48 | 49 | pub fn block_height(&mut self, val: u64) -> &Self { 50 | self.block_height = val; 51 | self 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /components/runes/src/db/models/db_balance_change.rs: -------------------------------------------------------------------------------- 1 | use postgres::types::{PgBigIntU32, PgNumericU128, PgNumericU64}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct DbBalanceChange { 5 | pub rune_id: String, 6 | pub block_height: PgNumericU64, 7 | pub address: String, 8 | pub balance: PgNumericU128, 9 | pub total_operations: PgBigIntU32, 10 | } 11 | 12 | impl DbBalanceChange { 13 | pub fn from_operation( 14 | rune_id: String, 15 | block_height: PgNumericU64, 16 | address: String, 17 | balance: PgNumericU128, 18 | ) -> Self { 19 | DbBalanceChange { 20 | rune_id, 21 | block_height, 22 | address, 23 | balance, 24 | total_operations: PgBigIntU32(1), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/runes/src/db/models/db_ledger_entry.rs: -------------------------------------------------------------------------------- 1 | use ordinals_parser::RuneId; 2 | use postgres::types::{PgBigIntU32, PgNumericU128, PgNumericU64}; 3 | use tokio_postgres::Row; 4 | 5 | use super::db_ledger_operation::DbLedgerOperation; 6 | 7 | /// A row in the `ledger` table. 8 | #[derive(Debug, Clone)] 9 | pub struct DbLedgerEntry { 10 | pub rune_id: String, 11 | pub block_hash: String, 12 | pub block_height: PgNumericU64, 13 | pub tx_index: PgBigIntU32, 14 | pub event_index: PgBigIntU32, 15 | pub tx_id: String, 16 | pub output: Option, 17 | pub address: Option, 18 | pub receiver_address: Option, 19 | pub amount: Option, 20 | pub operation: DbLedgerOperation, 21 | pub timestamp: PgBigIntU32, 22 | } 23 | 24 | impl DbLedgerEntry { 25 | pub fn from_values( 26 | amount: Option, 27 | rune_id: RuneId, 28 | block_hash: &String, 29 | block_height: u64, 30 | tx_index: u32, 31 | event_index: u32, 32 | tx_id: &String, 33 | output: Option, 34 | address: Option<&String>, 35 | receiver_address: Option<&String>, 36 | operation: DbLedgerOperation, 37 | timestamp: u32, 38 | ) -> Self { 39 | DbLedgerEntry { 40 | rune_id: rune_id.to_string(), 41 | block_hash: block_hash[2..].to_string(), 42 | block_height: PgNumericU64(block_height), 43 | tx_index: PgBigIntU32(tx_index), 44 | event_index: PgBigIntU32(event_index), 45 | tx_id: tx_id[2..].to_string(), 46 | output: output.map(PgBigIntU32), 47 | address: address.cloned(), 48 | receiver_address: receiver_address.cloned(), 49 | amount: amount.map(PgNumericU128), 50 | operation, 51 | timestamp: PgBigIntU32(timestamp), 52 | } 53 | } 54 | 55 | pub fn from_pg_row(row: &Row) -> Self { 56 | DbLedgerEntry { 57 | rune_id: row.get("rune_id"), 58 | block_hash: row.get("block_hash"), 59 | block_height: row.get("block_height"), 60 | tx_index: row.get("tx_index"), 61 | event_index: row.get("event_index"), 62 | tx_id: row.get("tx_id"), 63 | output: row.get("output"), 64 | address: row.get("address"), 65 | receiver_address: row.get("receiver_address"), 66 | amount: row.get("amount"), 67 | operation: row.get("operation"), 68 | timestamp: row.get("timestamp"), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /components/runes/src/db/models/db_ledger_operation.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use bytes::BytesMut; 4 | use tokio_postgres::types::{to_sql_checked, FromSql, IsNull, ToSql, Type}; 5 | 6 | /// A value from the `ledger_operation` enum type. 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub enum DbLedgerOperation { 9 | Etching, 10 | Mint, 11 | Burn, 12 | Send, 13 | Receive, 14 | } 15 | 16 | impl fmt::Display for DbLedgerOperation { 17 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 18 | write!(f, "{}", self.as_str().to_uppercase()) 19 | } 20 | } 21 | 22 | impl DbLedgerOperation { 23 | pub fn as_str(&self) -> &str { 24 | match self { 25 | Self::Etching => "etching", 26 | Self::Mint => "mint", 27 | Self::Burn => "burn", 28 | Self::Send => "send", 29 | Self::Receive => "receive", 30 | } 31 | } 32 | } 33 | 34 | impl std::str::FromStr for DbLedgerOperation { 35 | type Err = (); 36 | 37 | fn from_str(s: &str) -> Result { 38 | match s { 39 | "etching" => Ok(DbLedgerOperation::Etching), 40 | "mint" => Ok(DbLedgerOperation::Mint), 41 | "burn" => Ok(DbLedgerOperation::Burn), 42 | "send" => Ok(DbLedgerOperation::Send), 43 | "receive" => Ok(DbLedgerOperation::Receive), 44 | _ => Err(()), 45 | } 46 | } 47 | } 48 | 49 | impl ToSql for DbLedgerOperation { 50 | fn to_sql( 51 | &self, 52 | _ty: &Type, 53 | out: &mut BytesMut, 54 | ) -> Result> { 55 | out.extend_from_slice(self.as_str().as_bytes()); 56 | Ok(IsNull::No) 57 | } 58 | 59 | fn accepts(ty: &Type) -> bool { 60 | ty.name() == "ledger_operation" 61 | } 62 | 63 | to_sql_checked!(); 64 | } 65 | 66 | impl<'a> FromSql<'a> for DbLedgerOperation { 67 | fn from_sql( 68 | _ty: &Type, 69 | raw: &'a [u8], 70 | ) -> Result> { 71 | let s = std::str::from_utf8(raw)?; 72 | s.parse::() 73 | .map_err(|_| "failed to parse enum variant".into()) 74 | } 75 | 76 | fn accepts(ty: &Type) -> bool { 77 | ty.name() == "ledger_operation" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /components/runes/src/db/models/db_supply_change.rs: -------------------------------------------------------------------------------- 1 | use postgres::types::{PgNumericU128, PgNumericU64}; 2 | 3 | /// An update to a rune that affects its total counts. 4 | #[derive(Debug, Clone)] 5 | pub struct DbSupplyChange { 6 | pub rune_id: String, 7 | pub block_height: PgNumericU64, 8 | pub minted: PgNumericU128, 9 | pub total_mints: PgNumericU128, 10 | pub burned: PgNumericU128, 11 | pub total_burns: PgNumericU128, 12 | pub total_operations: PgNumericU128, 13 | } 14 | 15 | impl DbSupplyChange { 16 | pub fn from_mint(id: String, block_height: PgNumericU64, amount: PgNumericU128) -> Self { 17 | DbSupplyChange { 18 | rune_id: id, 19 | block_height, 20 | minted: amount, 21 | total_mints: PgNumericU128(1), 22 | burned: PgNumericU128(0), 23 | total_burns: PgNumericU128(0), 24 | total_operations: PgNumericU128(1), 25 | } 26 | } 27 | 28 | pub fn from_burn(id: String, block_height: PgNumericU64, amount: PgNumericU128) -> Self { 29 | DbSupplyChange { 30 | rune_id: id, 31 | block_height, 32 | minted: PgNumericU128(0), 33 | total_mints: PgNumericU128(0), 34 | burned: amount, 35 | total_burns: PgNumericU128(1), 36 | total_operations: PgNumericU128(1), 37 | } 38 | } 39 | 40 | pub fn from_operation(id: String, block_height: PgNumericU64) -> Self { 41 | DbSupplyChange { 42 | rune_id: id, 43 | block_height, 44 | minted: PgNumericU128(0), 45 | total_mints: PgNumericU128(0), 46 | burned: PgNumericU128(0), 47 | total_burns: PgNumericU128(0), 48 | total_operations: PgNumericU128(1), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /components/runes/src/db/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db_balance_change; 2 | pub mod db_ledger_entry; 3 | pub mod db_ledger_operation; 4 | pub mod db_rune; 5 | pub mod db_supply_change; 6 | -------------------------------------------------------------------------------- /components/runes/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod monitoring; 2 | -------------------------------------------------------------------------------- /dockerfiles/components/bitcoin-indexer.dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:bullseye AS build 2 | 3 | WORKDIR /src 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y \ 7 | wget && \ 8 | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - && \ 9 | echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-18 main" >> /etc/apt/sources.list.d/llvm.list && \ 10 | apt-get update && \ 11 | apt-get install -y \ 12 | pkg-config \ 13 | libssl-dev \ 14 | libunwind-dev \ 15 | libunwind8 \ 16 | curl \ 17 | libsnappy-dev \ 18 | libgflags-dev \ 19 | zlib1g-dev \ 20 | libbz2-dev \ 21 | liblz4-dev \ 22 | libzstd-dev \ 23 | clang-18 \ 24 | libclang-18-dev \ 25 | llvm-18-dev 26 | RUN rustup update 1.85 && rustup default 1.85 27 | 28 | RUN mkdir /out 29 | COPY ./Cargo.toml /src/Cargo.toml 30 | COPY ./Cargo.lock /src/Cargo.lock 31 | COPY ./components /src/components 32 | COPY ./migrations /src/migrations 33 | 34 | RUN cargo build --features release --release 35 | RUN cp /src/target/release/bitcoin-indexer /out 36 | 37 | FROM debian:bullseye-slim 38 | 39 | # Install runtime dependencies for LLVM/Clang 18 and other necessary libs 40 | RUN apt-get update && \ 41 | apt-get install -y \ 42 | gnupg \ 43 | ca-certificates \ 44 | wget && \ 45 | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - && \ 46 | echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-18 main" >> /etc/apt/sources.list.d/llvm.list && \ 47 | apt-get update && \ 48 | apt-get install -y \ 49 | pkg-config \ 50 | libssl-dev \ 51 | libunwind-dev \ 52 | libunwind8 \ 53 | libsnappy-dev \ 54 | libgflags-dev \ 55 | zlib1g-dev \ 56 | libbz2-dev \ 57 | liblz4-dev \ 58 | libzstd-dev \ 59 | clang-18 \ 60 | libclang-18-dev \ 61 | llvm-18-dev 62 | 63 | COPY --from=build /out/bitcoin-indexer /bin/bitcoin-indexer 64 | 65 | WORKDIR /workspace 66 | 67 | ENTRYPOINT ["bitcoin-indexer"] 68 | -------------------------------------------------------------------------------- /dockerfiles/components/ordinals-api.dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | COPY ./api/ordinals /app 5 | COPY .git /.git 6 | 7 | RUN apk add --no-cache --virtual .build-deps git 8 | RUN npm ci --no-audit && \ 9 | npm run build && \ 10 | npm run generate:git-info && \ 11 | npm prune --production 12 | RUN apk del .build-deps 13 | 14 | CMD ["node", "./dist/src/index.js"] 15 | -------------------------------------------------------------------------------- /dockerfiles/components/runes-api.dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | COPY ./api/runes /app 5 | COPY .git /.git 6 | 7 | RUN apk add --no-cache --virtual .build-deps git 8 | RUN npm ci --no-audit && \ 9 | npm run build && \ 10 | npm run generate:git-info && \ 11 | npm prune --production 12 | RUN apk del .build-deps 13 | 14 | CMD ["node", "./dist/src/index.js"] 15 | -------------------------------------------------------------------------------- /dockerfiles/docker-compose.dev.postgres.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | postgres: 4 | image: "postgres:15" 5 | ports: 6 | - "5432:5432" 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: postgres 10 | POSTGRES_DB: postgres 11 | POSTGRES_PORT: 5432 12 | -------------------------------------------------------------------------------- /migrations/ordinals-brc20/V1__tokens.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE tokens ( 2 | ticker TEXT NOT NULL PRIMARY KEY, 3 | display_ticker TEXT NOT NULL, 4 | inscription_id TEXT NOT NULL, 5 | inscription_number BIGINT NOT NULL, 6 | block_height NUMERIC NOT NULL, 7 | block_hash TEXT NOT NULL, 8 | tx_id TEXT NOT NULL, 9 | tx_index NUMERIC NOT NULL, 10 | address TEXT NOT NULL, 11 | max NUMERIC NOT NULL, 12 | "limit" NUMERIC NOT NULL, 13 | decimals SMALLINT NOT NULL, 14 | self_mint BOOLEAN NOT NULL DEFAULT FALSE, 15 | minted_supply NUMERIC DEFAULT 0, 16 | tx_count INT DEFAULT 0, 17 | timestamp BIGINT NOT NULL 18 | ); 19 | CREATE INDEX tokens_inscription_id_index ON tokens (inscription_id); 20 | CREATE INDEX tokens_block_height_tx_index_index ON tokens (block_height DESC, tx_index DESC); 21 | -------------------------------------------------------------------------------- /migrations/ordinals-brc20/V2__operations.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE operations ( 2 | ticker TEXT NOT NULL, 3 | operation TEXT NOT NULL, 4 | inscription_id TEXT NOT NULL, 5 | inscription_number BIGINT NOT NULL, 6 | ordinal_number NUMERIC NOT NULL, 7 | block_height NUMERIC NOT NULL, 8 | block_hash TEXT NOT NULL, 9 | tx_id TEXT NOT NULL, 10 | tx_index NUMERIC NOT NULL, 11 | output TEXT NOT NULL, 12 | "offset" NUMERIC NOT NULL, 13 | timestamp BIGINT NOT NULL, 14 | address TEXT NOT NULL, 15 | to_address TEXT, 16 | amount NUMERIC NOT NULL 17 | ); 18 | ALTER TABLE operations ADD PRIMARY KEY (inscription_id, operation); 19 | ALTER TABLE operations ADD CONSTRAINT operations_ticker_fk FOREIGN KEY(ticker) REFERENCES tokens(ticker) ON DELETE CASCADE; 20 | CREATE INDEX operations_operation_index ON operations (operation); 21 | CREATE INDEX operations_ticker_address_index ON operations (ticker, address); 22 | CREATE INDEX operations_block_height_tx_index_index ON operations (block_height DESC, tx_index DESC); 23 | CREATE INDEX operations_address_to_address_index ON operations (address, to_address); 24 | CREATE INDEX operations_ordinal_number_operation_index ON operations (ordinal_number, operation); 25 | -------------------------------------------------------------------------------- /migrations/ordinals-brc20/V3__balances.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE balances ( 2 | ticker TEXT NOT NULL, 3 | address TEXT NOT NULL, 4 | avail_balance NUMERIC NOT NULL, 5 | trans_balance NUMERIC NOT NULL, 6 | total_balance NUMERIC NOT NULL 7 | ); 8 | ALTER TABLE balances ADD PRIMARY KEY (ticker, address); 9 | ALTER TABLE balances ADD CONSTRAINT balances_ticker_fk FOREIGN KEY(ticker) REFERENCES tokens(ticker) ON DELETE CASCADE; 10 | CREATE INDEX balances_address_index ON balances (address); 11 | CREATE INDEX balances_ticker_total_balance_index ON balances (ticker, total_balance DESC); 12 | -------------------------------------------------------------------------------- /migrations/ordinals-brc20/V4__counts_by_operation.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_operation ( 2 | operation TEXT NOT NULL PRIMARY KEY, 3 | count INT NOT NULL DEFAULT 0 4 | ); 5 | -------------------------------------------------------------------------------- /migrations/ordinals-brc20/V5__counts_by_address_operation.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_address_operation ( 2 | address TEXT NOT NULL, 3 | operation TEXT NOT NULL, 4 | count INT NOT NULL DEFAULT 0 5 | ); 6 | ALTER TABLE counts_by_address_operation ADD PRIMARY KEY (address, operation); 7 | -------------------------------------------------------------------------------- /migrations/ordinals-brc20/V6__operations_to_address_index.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX operations_to_address_index ON operations (to_address); 2 | -------------------------------------------------------------------------------- /migrations/ordinals-brc20/V7__balances_history.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE balances_history ( 2 | ticker TEXT NOT NULL, 3 | address TEXT NOT NULL, 4 | block_height NUMERIC NOT NULL, 5 | avail_balance NUMERIC NOT NULL, 6 | trans_balance NUMERIC NOT NULL, 7 | total_balance NUMERIC NOT NULL 8 | ); 9 | ALTER TABLE balances_history ADD PRIMARY KEY (address, ticker, block_height); 10 | CREATE INDEX balances_history_block_height_index ON balances_history (block_height); 11 | -------------------------------------------------------------------------------- /migrations/ordinals/V10__counts_by_sat_rarity.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_sat_rarity ( 2 | rarity TEXT NOT NULL PRIMARY KEY, 3 | count INT NOT NULL DEFAULT 0 4 | ); 5 | -------------------------------------------------------------------------------- /migrations/ordinals/V11__counts_by_type.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_type ( 2 | type TEXT NOT NULL PRIMARY KEY, 3 | count INT NOT NULL DEFAULT 0 4 | ); 5 | -------------------------------------------------------------------------------- /migrations/ordinals/V12__counts_by_address.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_address ( 2 | address TEXT NOT NULL PRIMARY KEY, 3 | count INT NOT NULL DEFAULT 0 4 | ); 5 | -------------------------------------------------------------------------------- /migrations/ordinals/V13__counts_by_genesis_address.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_genesis_address ( 2 | address TEXT NOT NULL PRIMARY KEY, 3 | count INT NOT NULL DEFAULT 0 4 | ); 5 | -------------------------------------------------------------------------------- /migrations/ordinals/V14__counts_by_recursive.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_recursive ( 2 | recursive BOOLEAN NOT NULL PRIMARY KEY, 3 | count INT NOT NULL DEFAULT 0 4 | ); 5 | -------------------------------------------------------------------------------- /migrations/ordinals/V15__inscription_parents.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE inscription_parents ( 2 | inscription_id TEXT NOT NULL, 3 | parent_inscription_id TEXT NOT NULL 4 | ); 5 | ALTER TABLE inscription_parents ADD PRIMARY KEY (inscription_id, parent_inscription_id); 6 | ALTER TABLE inscription_parents ADD CONSTRAINT inscription_parents FOREIGN KEY(inscription_id) REFERENCES inscriptions(inscription_id) ON DELETE CASCADE; 7 | 8 | -- Migrate from old `parent` column in `inscriptions` table. 9 | INSERT INTO inscription_parents (inscription_id, parent_inscription_id) ( 10 | SELECT inscription_id, parent AS parent_inscription_id 11 | FROM inscriptions 12 | WHERE parent IS NOT NULL 13 | ); 14 | ALTER TABLE inscriptions DROP COLUMN parent; 15 | -------------------------------------------------------------------------------- /migrations/ordinals/V16__inscription_charms.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE inscriptions ADD COLUMN charms BIGINT NOT NULL DEFAULT 0; 2 | -------------------------------------------------------------------------------- /migrations/ordinals/V17__unbound_inscription_sequence.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE inscriptions ADD COLUMN unbound_sequence BIGINT UNIQUE; 2 | -------------------------------------------------------------------------------- /migrations/ordinals/V18__chain_tip_block_hash.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE chain_tip ADD COLUMN block_hash TEXT; 2 | ALTER TABLE chain_tip ALTER COLUMN block_height DROP NOT NULL; 3 | 4 | WITH last_block AS ( 5 | SELECT block_height, block_hash 6 | FROM locations 7 | ORDER BY block_height DESC 8 | LIMIT 1 9 | ) 10 | UPDATE chain_tip SET 11 | block_height = (SELECT block_height FROM last_block), 12 | block_hash = (SELECT block_hash FROM last_block); 13 | -------------------------------------------------------------------------------- /migrations/ordinals/V1__satoshis.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE satoshis ( 2 | ordinal_number NUMERIC NOT NULL PRIMARY KEY, 3 | rarity TEXT NOT NULL, 4 | coinbase_height NUMERIC NOT NULL 5 | ); 6 | CREATE INDEX satoshis_rarity_index ON satoshis (rarity); 7 | -------------------------------------------------------------------------------- /migrations/ordinals/V2__inscriptions.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE inscriptions ( 2 | inscription_id TEXT NOT NULL PRIMARY KEY, 3 | ordinal_number NUMERIC NOT NULL, 4 | number BIGINT NOT NULL UNIQUE, 5 | classic_number BIGINT NOT NULL UNIQUE, 6 | block_height NUMERIC NOT NULL, 7 | block_hash TEXT NOT NULL, 8 | tx_id TEXT NOT NULL, 9 | tx_index BIGINT NOT NULL, 10 | address TEXT, 11 | mime_type TEXT NOT NULL, 12 | content_type TEXT NOT NULL, 13 | content_length BIGINT NOT NULL, 14 | content BYTEA NOT NULL, 15 | fee NUMERIC NOT NULL, 16 | curse_type TEXT, 17 | recursive BOOLEAN DEFAULT FALSE, 18 | input_index BIGINT NOT NULL, 19 | pointer NUMERIC, 20 | metadata TEXT, 21 | metaprotocol TEXT, 22 | parent TEXT, 23 | delegate TEXT, 24 | timestamp BIGINT NOT NULL 25 | ); 26 | CREATE INDEX inscriptions_mime_type_index ON inscriptions (mime_type); 27 | CREATE INDEX inscriptions_recursive_index ON inscriptions (recursive); 28 | CREATE INDEX inscriptions_block_height_tx_index_index ON inscriptions (block_height DESC, tx_index DESC); 29 | CREATE INDEX inscriptions_address_index ON inscriptions (address); 30 | CREATE INDEX inscriptions_ordinal_number_index ON inscriptions (ordinal_number); 31 | -------------------------------------------------------------------------------- /migrations/ordinals/V3__locations.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE locations ( 2 | ordinal_number NUMERIC NOT NULL, 3 | block_height NUMERIC NOT NULL, 4 | tx_index BIGINT NOT NULL, 5 | tx_id TEXT NOT NULL, 6 | block_hash TEXT NOT NULL, 7 | address TEXT, 8 | output TEXT NOT NULL, 9 | "offset" NUMERIC, 10 | prev_output TEXT, 11 | prev_offset NUMERIC, 12 | value NUMERIC, 13 | transfer_type TEXT NOT NULL, 14 | timestamp BIGINT NOT NULL 15 | ); 16 | ALTER TABLE locations ADD PRIMARY KEY (ordinal_number, block_height, tx_index); 17 | CREATE INDEX locations_output_offset_index ON locations (output, "offset"); 18 | CREATE INDEX locations_timestamp_index ON locations (timestamp); 19 | CREATE INDEX locations_block_height_tx_index_index ON locations (block_height DESC, tx_index DESC); 20 | -------------------------------------------------------------------------------- /migrations/ordinals/V4__current_locations.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE current_locations ( 2 | ordinal_number NUMERIC NOT NULL PRIMARY KEY, 3 | block_height NUMERIC NOT NULL, 4 | tx_id TEXT NOT NULL, 5 | tx_index BIGINT NOT NULL, 6 | address TEXT, 7 | output TEXT NOT NULL, 8 | "offset" NUMERIC 9 | ); 10 | CREATE INDEX current_locations_address_index ON current_locations (address); 11 | CREATE INDEX current_locations_block_height_tx_index_index ON current_locations (block_height, tx_index); 12 | CREATE INDEX current_locations_output_index ON current_locations (output); 13 | -------------------------------------------------------------------------------- /migrations/ordinals/V5__inscription_transfers.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE inscription_transfers ( 2 | inscription_id TEXT NOT NULL, 3 | number BIGINT NOT NULL, 4 | ordinal_number NUMERIC NOT NULL, 5 | block_height NUMERIC NOT NULL, 6 | tx_index BIGINT NOT NULL, 7 | from_block_height NUMERIC NOT NULL, 8 | from_tx_index BIGINT NOT NULL, 9 | block_transfer_index INT NOT NULL 10 | ); 11 | ALTER TABLE inscription_transfers ADD PRIMARY KEY (block_height, block_transfer_index); 12 | CREATE INDEX inscription_transfers_inscription_id_index ON inscription_transfers (inscription_id); 13 | CREATE INDEX inscription_transfers_number_index ON inscription_transfers (number); 14 | -------------------------------------------------------------------------------- /migrations/ordinals/V6__chain_tip.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE chain_tip ( 2 | id BOOLEAN PRIMARY KEY DEFAULT TRUE, 3 | block_height NUMERIC NOT NULL DEFAULT 0 4 | ); 5 | ALTER TABLE chain_tip ADD CONSTRAINT chain_tip_one_row CHECK(id); 6 | 7 | INSERT INTO chain_tip DEFAULT VALUES; 8 | -------------------------------------------------------------------------------- /migrations/ordinals/V7__inscription_recursions.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE inscription_recursions ( 2 | inscription_id TEXT NOT NULL, 3 | ref_inscription_id TEXT NOT NULL 4 | ); 5 | ALTER TABLE inscription_recursions ADD PRIMARY KEY (inscription_id, ref_inscription_id); 6 | ALTER TABLE inscription_recursions ADD CONSTRAINT inscription_recursions_inscription_id_fk FOREIGN KEY(inscription_id) REFERENCES inscriptions(inscription_id) ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/ordinals/V8__counts_by_block.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_block ( 2 | block_height NUMERIC NOT NULL PRIMARY KEY, 3 | block_hash TEXT NOT NULL, 4 | inscription_count INT NOT NULL, 5 | inscription_count_accum INT NOT NULL, 6 | timestamp BIGINT NOT NULL 7 | ); 8 | CREATE INDEX counts_by_block_block_hash_index ON counts_by_block (block_hash); 9 | -------------------------------------------------------------------------------- /migrations/ordinals/V9__counts_by_mime_type.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE counts_by_mime_type ( 2 | mime_type TEXT NOT NULL PRIMARY KEY, 3 | count INT NOT NULL DEFAULT 0 4 | ); 5 | -------------------------------------------------------------------------------- /migrations/runes/V1__runes.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS runes ( 2 | id TEXT NOT NULL PRIMARY KEY, 3 | number BIGINT NOT NULL UNIQUE, 4 | name TEXT NOT NULL UNIQUE, 5 | spaced_name TEXT NOT NULL UNIQUE, 6 | block_hash TEXT NOT NULL, 7 | block_height NUMERIC NOT NULL, 8 | tx_index BIGINT NOT NULL, 9 | tx_id TEXT NOT NULL, 10 | divisibility SMALLINT NOT NULL DEFAULT 0, 11 | premine NUMERIC NOT NULL DEFAULT 0, 12 | symbol TEXT NOT NULL DEFAULT '¤', 13 | terms_amount NUMERIC, 14 | terms_cap NUMERIC, 15 | terms_height_start NUMERIC, 16 | terms_height_end NUMERIC, 17 | terms_offset_start NUMERIC, 18 | terms_offset_end NUMERIC, 19 | turbo BOOLEAN NOT NULL DEFAULT FALSE, 20 | cenotaph BOOLEAN NOT NULL DEFAULT FALSE, 21 | timestamp BIGINT NOT NULL 22 | ); 23 | 24 | CREATE INDEX runes_block_height_tx_index_index ON runes (block_height DESC, tx_index DESC); 25 | 26 | -- Insert default 'UNCOMMON•GOODS' 27 | INSERT INTO runes ( 28 | id, number, name, spaced_name, block_hash, block_height, tx_index, tx_id, symbol, terms_amount, 29 | terms_cap, terms_height_start, terms_height_end, timestamp 30 | ) 31 | VALUES ( 32 | '1:0', 0, 'UNCOMMONGOODS', 'UNCOMMON•GOODS', 33 | '0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5', 840000, 0, '', '⧉', 1, 34 | '340282366920938463463374607431768211455', 840000, 1050000, 0 35 | ); 36 | -------------------------------------------------------------------------------- /migrations/runes/V2__supply_changes.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS supply_changes ( 2 | rune_id TEXT NOT NULL, 3 | block_height NUMERIC NOT NULL, 4 | minted NUMERIC NOT NULL DEFAULT 0, 5 | total_mints NUMERIC NOT NULL DEFAULT 0, 6 | burned NUMERIC NOT NULL DEFAULT 0, 7 | total_burns NUMERIC NOT NULL DEFAULT 0, 8 | total_operations NUMERIC NOT NULL DEFAULT 0, 9 | PRIMARY KEY (rune_id, block_height) 10 | ); 11 | -------------------------------------------------------------------------------- /migrations/runes/V3__ledger.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE ledger_operation AS ENUM ('etching', 'mint', 'burn', 'send', 'receive'); 2 | 3 | CREATE TABLE IF NOT EXISTS ledger ( 4 | rune_id TEXT NOT NULL, 5 | block_hash TEXT NOT NULL, 6 | block_height NUMERIC NOT NULL, 7 | tx_index BIGINT NOT NULL, 8 | event_index BIGINT NOT NULL, 9 | tx_id TEXT NOT NULL, 10 | output BIGINT, 11 | address TEXT, 12 | receiver_address TEXT, 13 | amount NUMERIC, 14 | operation ledger_operation NOT NULL, 15 | timestamp BIGINT NOT NULL 16 | ); 17 | 18 | CREATE INDEX ledger_rune_id_index ON ledger (rune_id); 19 | CREATE INDEX ledger_block_height_tx_index_event_index_index ON ledger (block_height DESC, tx_index DESC, event_index DESC); 20 | CREATE INDEX ledger_address_rune_id_index ON ledger (address, rune_id); 21 | CREATE INDEX ledger_tx_id_output_index ON ledger (tx_id, output); 22 | -------------------------------------------------------------------------------- /migrations/runes/V4__balance_changes.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS balance_changes ( 2 | rune_id TEXT NOT NULL, 3 | block_height NUMERIC NOT NULL, 4 | address TEXT NOT NULL, 5 | balance NUMERIC NOT NULL, 6 | total_operations BIGINT NOT NULL DEFAULT 0, 7 | PRIMARY KEY (rune_id, block_height, address) 8 | ); 9 | 10 | CREATE INDEX balance_changes_address_balance_index ON balance_changes (address, block_height, balance DESC); 11 | CREATE INDEX balance_changes_rune_id_balance_index ON balance_changes (rune_id, block_height, balance DESC); 12 | -------------------------------------------------------------------------------- /ordhook.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "../ordinals-api" 8 | } 9 | ], 10 | "settings": {} 11 | } -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Start PostgreSQL container 5 | echo "Starting PostgreSQL environment..." 6 | docker compose -f dockerfiles/docker-compose.dev.postgres.yml up -d 7 | echo "PostgreSQL containers started" 8 | 9 | # Build the image targeting the build stage 10 | echo "Building Bitcoin Indexer image..." 11 | docker build --no-cache --target build -t test-bitcoin-indexer -f dockerfiles/components/bitcoin-indexer.dockerfile . 12 | 13 | # Run the tests 14 | echo "Running tests..." 15 | docker run -it --rm \ 16 | -v $(pwd):/src \ 17 | -v /var/run/docker.sock:/var/run/docker.sock \ 18 | --network host \ 19 | test-bitcoin-indexer \ 20 | bash -c "cd /src && RUST_BACKTRACE=1 cargo test --workspace --color=always --no-fail-fast -- --nocapture --show-output" 21 | 22 | # Clean up 23 | echo "Cleaning up containers..." 24 | docker compose -f dockerfiles/docker-compose.dev.postgres.yml down -v -t 0 25 | echo "Test environment cleanup complete" 26 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "deploymentEnabled": false 4 | } 5 | } 6 | --------------------------------------------------------------------------------