From 9eef3c84082beae71b05f2ddd078d84cca59c8d1 Mon Sep 17 00:00:00 2001 From: ayo-adigun Date: Fri, 19 Sep 2025 14:02:14 +0100 Subject: [PATCH] Blockchain Explorer Smart Contract --- blockchain/.gitignore | 4 + blockchain/.vscode/settings.json | 4 + blockchain/.vscode/tasks.json | 18 ++ blockchain/Clarinet.toml | 11 ++ blockchain/contracts/explorer.clar | 296 +++++++++++++++++++++++++++++ blockchain/settings/Devnet.toml | 127 +++++++++++++ blockchain/tests/explorer_test.ts | 26 +++ 7 files changed, 486 insertions(+) create mode 100644 blockchain/.gitignore create mode 100644 blockchain/.vscode/settings.json create mode 100644 blockchain/.vscode/tasks.json create mode 100644 blockchain/Clarinet.toml create mode 100644 blockchain/contracts/explorer.clar create mode 100644 blockchain/settings/Devnet.toml create mode 100644 blockchain/tests/explorer_test.ts diff --git a/blockchain/.gitignore b/blockchain/.gitignore new file mode 100644 index 0000000..f18b582 --- /dev/null +++ b/blockchain/.gitignore @@ -0,0 +1,4 @@ + +settings/Mainnet.toml +settings/Testnet.toml +history.txt diff --git a/blockchain/.vscode/settings.json b/blockchain/.vscode/settings.json new file mode 100644 index 0000000..02e21eb --- /dev/null +++ b/blockchain/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "deno.enable": true, +} diff --git a/blockchain/.vscode/tasks.json b/blockchain/.vscode/tasks.json new file mode 100644 index 0000000..22af91c --- /dev/null +++ b/blockchain/.vscode/tasks.json @@ -0,0 +1,18 @@ + +{ + "version": "2.0.0", + "tasks": [ + { + "label": "check contracts", + "group": "test", + "type": "shell", + "command": "clarinet check" + }, + { + "label": "test contracts", + "group": "test", + "type": "shell", + "command": "clarinet test" + } + ] +} diff --git a/blockchain/Clarinet.toml b/blockchain/Clarinet.toml new file mode 100644 index 0000000..a13181f --- /dev/null +++ b/blockchain/Clarinet.toml @@ -0,0 +1,11 @@ +[project] +name = "blockchain" +authors = [] +description = "" +telemetry = true +requirements = [] +analysis = ["check_checker"] +costs_version = 2 +[contracts.explorer] +path = "contracts/explorer.clar" +depends_on = [] diff --git a/blockchain/contracts/explorer.clar b/blockchain/contracts/explorer.clar new file mode 100644 index 0000000..0702434 --- /dev/null +++ b/blockchain/contracts/explorer.clar @@ -0,0 +1,296 @@ +;; Blockchain Explorer Contract +;; A comprehensive contract for exploring blockchain data + +;; Constants +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-NOT-AUTHORIZED (err u100)) +(define-constant ERR-INVALID-BLOCK (err u101)) +(define-constant ERR-BLOCK-NOT-FOUND (err u102)) +(define-constant ERR-INVALID-INPUT (err u103)) +(define-constant ERR-DATA-EXISTS (err u104)) +(define-constant MAX-BLOCKS u1000000) +(define-constant MAX-TXS-PER-BLOCK u100) + +;; Data Variables +(define-data-var contract-active bool true) +(define-data-var total-blocks-indexed uint u0) +(define-data-var last-indexed-block uint u0) + +;; Data Maps +(define-map block-info + { block-height: uint } + { + block-hash: (buff 32), + timestamp: uint, + tx-count: uint, + miner: principal, + size: uint, + indexed-at: uint + } +) + +(define-map transaction-info + { tx-id: (buff 32) } + { + block-height: uint, + sender: principal, + recipient: (optional principal), + amount: uint, + fee: uint, + status: (string-ascii 20), + tx-type: (string-ascii 30) + } +) + +(define-map address-stats + { address: principal } + { + tx-count: uint, + total-sent: uint, + total-received: uint, + first-seen: uint, + last-active: uint + } +) + +(define-map block-transactions + { block-height: uint, tx-index: uint } + { tx-id: (buff 32) } +) + +;; Authorization Functions +(define-private (is-contract-owner) + (is-eq tx-sender CONTRACT-OWNER) +) + +(define-private (is-contract-active) + (var-get contract-active) +) + +;; Block Management Functions +(define-public (index-block (height uint) (hash (buff 32)) (timestamp uint) + (tx-count uint) (miner principal) (size uint)) + (begin + (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED) + (asserts! (is-contract-active) ERR-NOT-AUTHORIZED) + (asserts! (< height MAX-BLOCKS) ERR-INVALID-BLOCK) + (asserts! (> tx-count u0) ERR-INVALID-INPUT) + (asserts! (is-none (map-get? block-info { block-height: height })) ERR-DATA-EXISTS) + + (map-set block-info + { block-height: height } + { + block-hash: hash, + timestamp: timestamp, + tx-count: tx-count, + miner: miner, + size: size, + indexed-at: block-height + } + ) + + (var-set total-blocks-indexed (+ (var-get total-blocks-indexed) u1)) + (var-set last-indexed-block (if (> height (var-get last-indexed-block)) + height + (var-get last-indexed-block))) + (ok height) + ) +) + +(define-public (index-transaction (tx-id (buff 32)) (block-height uint) (sender principal) + (recipient (optional principal)) (amount uint) (fee uint) + (status (string-ascii 20)) (tx-type (string-ascii 30))) + (begin + (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED) + (asserts! (is-contract-active) ERR-NOT-AUTHORIZED) + (asserts! (is-some (map-get? block-info { block-height: block-height })) ERR-BLOCK-NOT-FOUND) + + (map-set transaction-info + { tx-id: tx-id } + { + block-height: block-height, + sender: sender, + recipient: recipient, + amount: amount, + fee: fee, + status: status, + tx-type: tx-type + } + ) + + (update-address-stats sender amount true block-height) + (match recipient + addr (update-address-stats addr amount false block-height) + true + ) + (ok true) + ) +) + +;; Private helper for address statistics +(define-private (update-address-stats (addr principal) (amount uint) (is-sender bool) (height uint)) + (let ((existing-stats (default-to + { tx-count: u0, total-sent: u0, total-received: u0, + first-seen: height, last-active: height } + (map-get? address-stats { address: addr })))) + (map-set address-stats + { address: addr } + { + tx-count: (+ (get tx-count existing-stats) u1), + total-sent: (if is-sender + (+ (get total-sent existing-stats) amount) + (get total-sent existing-stats)), + total-received: (if is-sender + (get total-received existing-stats) + (+ (get total-received existing-stats) amount)), + first-seen: (if (< height (get first-seen existing-stats)) + height + (get first-seen existing-stats)), + last-active: (if (> height (get last-active existing-stats)) + height + (get last-active existing-stats)) + } + ) + ) +) + +;; Query Functions +(define-read-only (get-block-info (height uint)) + (map-get? block-info { block-height: height }) +) + +(define-read-only (get-transaction-info (tx-id (buff 32))) + (map-get? transaction-info { tx-id: tx-id }) +) + +(define-read-only (get-address-stats (addr principal)) + (map-get? address-stats { address: addr }) +) + +(define-read-only (get-latest-block) + (var-get last-indexed-block) +) + +(define-read-only (get-total-indexed-blocks) + (var-get total-blocks-indexed) +) + +;; Simple block range query (returns up to 10 blocks) +(define-read-only (get-block-range (start uint) (end uint)) + (begin + (asserts! (<= start end) (err ERR-INVALID-INPUT)) + (asserts! (<= (- end start) u10) (err ERR-INVALID-INPUT)) + (ok { + block-0: (if (>= start (+ start u0)) (map-get? block-info { block-height: start }) none), + block-1: (if (>= end (+ start u1)) (map-get? block-info { block-height: (+ start u1) }) none), + block-2: (if (>= end (+ start u2)) (map-get? block-info { block-height: (+ start u2) }) none), + block-3: (if (>= end (+ start u3)) (map-get? block-info { block-height: (+ start u3) }) none), + block-4: (if (>= end (+ start u4)) (map-get? block-info { block-height: (+ start u4) }) none), + block-5: (if (>= end (+ start u5)) (map-get? block-info { block-height: (+ start u5) }) none), + block-6: (if (>= end (+ start u6)) (map-get? block-info { block-height: (+ start u6) }) none), + block-7: (if (>= end (+ start u7)) (map-get? block-info { block-height: (+ start u7) }) none), + block-8: (if (>= end (+ start u8)) (map-get? block-info { block-height: (+ start u8) }) none), + block-9: (if (>= end (+ start u9)) (map-get? block-info { block-height: (+ start u9) }) none) + }) + ) +) + +;; Simple miner search (checks specific block heights) +(define-read-only (search-blocks-by-miner (miner principal) (start-height uint)) + (begin + (asserts! (<= start-height (var-get last-indexed-block)) (err ERR-INVALID-INPUT)) + (let ((end-height (if (< (+ start-height u10) (var-get last-indexed-block)) + (+ start-height u10) + (var-get last-indexed-block)))) + (ok { + matches: (list + (check-block-miner miner start-height) + (check-block-miner miner (+ start-height u1)) + (check-block-miner miner (+ start-height u2)) + (check-block-miner miner (+ start-height u3)) + (check-block-miner miner (+ start-height u4)) + (check-block-miner miner (+ start-height u5)) + (check-block-miner miner (+ start-height u6)) + (check-block-miner miner (+ start-height u7)) + (check-block-miner miner (+ start-height u8)) + (check-block-miner miner (+ start-height u9)) + ), + search-range: { start: start-height, end: end-height } + }) + ) + ) +) + +(define-private (check-block-miner (target-miner principal) (height uint)) + (match (map-get? block-info { block-height: height }) + block-data (if (is-eq (get miner block-data) target-miner) + (some height) + none) + none + ) +) + +(define-read-only (get-address-balance (addr principal)) + (match (get-address-stats addr) + stats (ok (- (get total-received stats) (get total-sent stats))) + (err ERR-INVALID-INPUT) + ) +) + +(define-read-only (get-network-stats) + (ok { + total-blocks: (var-get total-blocks-indexed), + latest-block: (var-get last-indexed-block), + contract-active: (var-get contract-active) + }) +) + +;; Administrative Functions +(define-public (toggle-contract-status) + (begin + (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED) + (var-set contract-active (not (var-get contract-active))) + (ok (var-get contract-active)) + ) +) + +(define-public (bulk-index-transactions (tx-list (list 10 { tx-id: (buff 32), block-height: uint, + sender: principal, recipient: (optional principal), + amount: uint, fee: uint, status: (string-ascii 20), + tx-type: (string-ascii 30) }))) + (begin + (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED) + (asserts! (is-contract-active) ERR-NOT-AUTHORIZED) + (ok (map process-bulk-tx tx-list)) + ) +) + +(define-private (process-bulk-tx (tx-data { tx-id: (buff 32), block-height: uint, sender: principal, + recipient: (optional principal), amount: uint, fee: uint, + status: (string-ascii 20), tx-type: (string-ascii 30) })) + (index-transaction + (get tx-id tx-data) + (get block-height tx-data) + (get sender tx-data) + (get recipient tx-data) + (get amount tx-data) + (get fee tx-data) + (get status tx-data) + (get tx-type tx-data) + ) +) + +;; Utility Functions +(define-read-only (validate-block-height (height uint)) + (and (> height u0) (<= height MAX-BLOCKS)) +) + +(define-read-only (get-contract-info) + (ok { + owner: CONTRACT-OWNER, + version: "1.0.0", + active: (var-get contract-active), + max-blocks: MAX-BLOCKS, + max-txs-per-block: MAX-TXS-PER-BLOCK + }) +) \ No newline at end of file diff --git a/blockchain/settings/Devnet.toml b/blockchain/settings/Devnet.toml new file mode 100644 index 0000000..8a5ff75 --- /dev/null +++ b/blockchain/settings/Devnet.toml @@ -0,0 +1,127 @@ +[network] +name = "devnet" +deployment_fee_rate = 10 + +[accounts.deployer] +mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +balance = 100_000_000_000_000 +# secret_key: 753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601 +# stx_address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM +# btc_address: mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH + +[accounts.wallet_1] +mnemonic = "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild" +balance = 100_000_000_000_000 +# secret_key: 7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801 +# stx_address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 +# btc_address: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC + +[accounts.wallet_2] +mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital" +balance = 100_000_000_000_000 +# secret_key: 530d9f61984c888536871c6573073bdfc0058896dc1adfe9a6a10dfacadc209101 +# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG +# btc_address: muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG + +[accounts.wallet_3] +mnemonic = "cycle puppy glare enroll cost improve round trend wrist mushroom scorpion tower claim oppose clever elephant dinosaur eight problem before frozen dune wagon high" +balance = 100_000_000_000_000 +# secret_key: d655b2523bcd65e34889725c73064feb17ceb796831c0e111ba1a552b0f31b3901 +# stx_address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC +# btc_address: mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7 + +[accounts.wallet_4] +mnemonic = "board list obtain sugar hour worth raven scout denial thunder horse logic fury scorpion fold genuine phrase wealth news aim below celery when cabin" +balance = 100_000_000_000_000 +# secret_key: f9d7206a47f14d2870c163ebab4bf3e70d18f5d14ce1031f3902fbbc894fe4c701 +# stx_address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND +# btc_address: mg1C76bNTutiCDV3t9nWhZs3Dc8LzUufj8 + +[accounts.wallet_5] +mnemonic = "hurry aunt blame peanut heavy update captain human rice crime juice adult scale device promote vast project quiz unit note reform update climb purchase" +balance = 100_000_000_000_000 +# secret_key: 3eccc5dac8056590432db6a35d52b9896876a3d5cbdea53b72400bc9c2099fe801 +# stx_address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB +# btc_address: mweN5WVqadScHdA81aATSdcVr4B6dNokqx + +[accounts.wallet_6] +mnemonic = "area desk dutch sign gold cricket dawn toward giggle vibrant indoor bench warfare wagon number tiny universe sand talk dilemma pottery bone trap buddy" +balance = 100_000_000_000_000 +# secret_key: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 +# stx_address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 +# btc_address: mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt + +[accounts.wallet_7] +mnemonic = "prevent gallery kind limb income control noise together echo rival record wedding sense uncover school version force bleak nuclear include danger skirt enact arrow" +balance = 100_000_000_000_000 +# secret_key: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 +# stx_address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ +# btc_address: n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7 + +[accounts.wallet_8] +mnemonic = "female adjust gallery certain visit token during great side clown fitness like hurt clip knife warm bench start reunion globe detail dream depend fortune" +balance = 100_000_000_000_000 +# secret_key: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 +# stx_address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP +# btc_address: n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw + +[accounts.wallet_9] +mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +balance = 100_000_000_000_000 +# secret_key: de433bdfa14ec43aa1098d5be594c8ffb20a31485ff9de2923b2689471c401b801 +# stx_address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 +# btc_address: mjSrB3wS4xab3kYqFktwBzfTdPg367ZJ2d + +[devnet] +disable_bitcoin_explorer = true +# disable_stacks_explorer = true +# disable_stacks_api = true +# working_dir = "tmp/devnet" +# stacks_node_events_observers = ["host.docker.internal:8002"] +# miner_mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +# miner_derivation_path = "m/44'/5757'/0'/0/0" +# orchestrator_port = 20445 +# bitcoin_node_p2p_port = 18444 +# bitcoin_node_rpc_port = 18443 +# bitcoin_node_username = "devnet" +# bitcoin_node_password = "devnet" +# bitcoin_controller_port = 18442 +# bitcoin_controller_block_time = 30_000 +# stacks_node_rpc_port = 20443 +# stacks_node_p2p_port = 20444 +# stacks_api_port = 3999 +# stacks_api_events_port = 3700 +# bitcoin_explorer_port = 8001 +# stacks_explorer_port = 8000 +# postgres_port = 5432 +# postgres_username = "postgres" +# postgres_password = "postgres" +# postgres_database = "postgres" +# bitcoin_node_image_url = "quay.io/hirosystems/bitcoind:devnet" +# stacks_node_image_url = "localhost:5000/stacks-node:devnet" +# stacks_api_image_url = "blockstack/stacks-blockchain-api:latest" +# stacks_explorer_image_url = "blockstack/explorer:latest" +# bitcoin_explorer_image_url = "quay.io/hirosystems/bitcoin-explorer:devnet" +# postgres_image_url = "postgres:alpine" + +# Send some stacking orders +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_1" +slots = 2 +btc_address = "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_2" +slots = 1 +btc_address = "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_3" +slots = 1 +btc_address = "mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7" diff --git a/blockchain/tests/explorer_test.ts b/blockchain/tests/explorer_test.ts new file mode 100644 index 0000000..9a18ae0 --- /dev/null +++ b/blockchain/tests/explorer_test.ts @@ -0,0 +1,26 @@ + +import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.14.0/index.ts'; +import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; + +Clarinet.test({ + name: "Ensure that <...>", + async fn(chain: Chain, accounts: Map) { + let block = chain.mineBlock([ + /* + * Add transactions with: + * Tx.contractCall(...) + */ + ]); + assertEquals(block.receipts.length, 0); + assertEquals(block.height, 2); + + block = chain.mineBlock([ + /* + * Add transactions with: + * Tx.contractCall(...) + */ + ]); + assertEquals(block.receipts.length, 0); + assertEquals(block.height, 3); + }, +});