diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2114a1f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Hemi Labs, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f09cc9 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Hemi Viem + +[Viem](https://viem.sh/) extension for the [Hemi Network](https://hemi.xyz/). +It includes: + +- Chain definitions! +- [Bitcoin finality](https://docs.hemi.xyz/foundational-topics/pop-consensus-and-bitcoin-finality) helper! +- [Bitcoin Kit](https://github.com/hemilabs/research/blob/main/research/Bitcoin-kit.md) wrappers! + +## Installation + +```sh +npm install viem hemi-viem +``` + +### Example + +```js +// example.js + +import { createPublicClient, http } from "viem" + +import { + hemiPublicBitcoinKitActions, + hemiPublicOpNodeActions, + hemiSepolia, +} from "hemi-viem" + +const parameters = { chain: hemiSepolia, transport: http() } +const client = createPublicClient(parameters) + .extend(hemiPublicOpNodeActions()) + .extend(hemiPublicBitcoinKitActions()) + +const blockNumber = await client.getBlockNumber() +const block = await client.getBlock({ blockNumber: blockNumber - 100n }) + +const btcFinality = await client.getBtcFinalityByBlockHash(block) +console.log(btcFinality) + +const btcHeader = await client.getLastHeader() +console.log(btcHeader) +``` + +Output: + +```console +$ node example.js +{ + l2_keystone: { + version: 1, + l1_block_number: 5868140, + l2_block_number: 596350, + parent_ep_hash: '3fe407f9eec38ce9fa0f6d159adca4ac8739013d380e1d7215b721c7e345ae88', + prev_ep_keystone_hash: '6b65018eb33048494f2e21ffdd081ae376b125f12e62ab88e77b1461503540fb', + state_root: 'b3b9f5b810038466290a7c4bff08a260a472d67fed230a27d89de1322490fb51', + ep_hash: 'd186c2da80f2c79490f644e4af714ee1f5790a4e806028cac0e64a2010a41974' + }, + btc_pub_height: 2814228, + btc_pub_header_hash: '1a262fa78618f4b7fa8d5d37eed32f3a0bb67ab09d808e035500000000000000', + btc_finality: 8 +} +{ + height: 2814236, + blockHash: '0x00000000f71ba71af42fb2a356affa1439bc2adea0bf67880fc30f28237622e7', + version: 536870912, + previousBlockHash: '0x000000000000002f3cc7a5f9ed589b734808fff6d67c0f8a9385f6146c80297b', + merkleRoot: '0x1799d4002cd58d15a764cee0919edf358bf36ace4fa4c6e78250336eafe12184', + timestamp: 1715273381, + bits: 486604799, + nonce: 4137790000 +} +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..95414a4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,158 @@ +{ + "name": "hemi-viem", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hemi-viem", + "version": "1.0.0", + "dependencies": { + "viem": "2.9.31" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", + "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", + "dependencies": { + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/abitype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.0.tgz", + "integrity": "sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/isows": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.3.tgz", + "integrity": "sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/viem": { + "version": "2.9.31", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.9.31.tgz", + "integrity": "sha512-8aJ8Dm/591Czwb/nRayo0z8Ls5KxqC4QYE33fmHwhx2tDUWC/hHcPZqjLRSTWFtAfi0aZKvP7BeB6UZ3ZkTRhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@scure/bip32": "1.3.2", + "@scure/bip39": "1.2.1", + "abitype": "1.0.0", + "isows": "1.0.3", + "ws": "8.13.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e584e55 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "hemi-viem", + "version": "1.0.0", + "description": "Viem extension for the Hemi Network", + "license": "MIT", + "author": { + "name": "Gabriel Montes", + "email": "gabriel@bloq.com" + }, + "main": "src/index.js", + "scripts": {}, + "dependencies": { + "viem": "^2.0.0" + }, + "prettier": { + "semi": false + }, + "type": "module" +} diff --git a/src/actions/bitcoin-kit.js b/src/actions/bitcoin-kit.js new file mode 100644 index 0000000..1027a2c --- /dev/null +++ b/src/actions/bitcoin-kit.js @@ -0,0 +1,63 @@ +import { + bitcoinKitTxsAbi, + bitcoinKitTxAddresses, +} from "../contracts/bitcoin-kit-txs.js" + +export function getBitcoinAddressBalance(client, parameters) { + const { btcAddress } = parameters + return client.readContract({ + address: bitcoinKitTxAddresses[client.chain.id], + abi: bitcoinKitTxsAbi, + functionName: "getBitcoinAddressBalance", + args: [btcAddress], + }) +} + +export function getHeaderN(client, parameters) { + const { height } = parameters + return client.readContract({ + address: bitcoinKitTxAddresses[client.chain.id], + abi: bitcoinKitTxsAbi, + functionName: "getHeaderN", + args: [height], + }) +} + +export function getLastHeader(client) { + return client.readContract({ + address: bitcoinKitTxAddresses[client.chain.id], + abi: bitcoinKitTxsAbi, + functionName: "getLastHeader", + args: [], + }) +} + +export function getTransactionByTxId(client, parameters) { + const { txId } = parameters + return client.readContract({ + address: bitcoinKitTxAddresses[client.chain.id], + abi: bitcoinKitTxsAbi, + functionName: "getTransactionByTxId", + args: [txId], + }) +} + +export function getTxConfirmations(client, parameters) { + const { txId } = parameters + return client.readContract({ + address: bitcoinKitTxAddresses[client.chain.id], + abi: bitcoinKitTxsAbi, + functionName: "getTxConfirmations", + args: [txId], + }) +} + +export function getUtxosForBitcoinAddress(client, parameters) { + const { btcAddress, pageNumber, pageSize } = parameters + return client.readContract({ + address: bitcoinKitTxAddresses[client.chain.id], + abi: bitcoinKitTxsAbi, + functionName: "getUTXOsForBitcoinAddress", + args: [btcAddress, pageNumber, pageSize], + }) +} diff --git a/src/actions/get-btc-finality-by-block-hash.js b/src/actions/get-btc-finality-by-block-hash.js new file mode 100644 index 0000000..8dc0577 --- /dev/null +++ b/src/actions/get-btc-finality-by-block-hash.js @@ -0,0 +1,25 @@ +import { http } from "viem" + +export async function getBtcFinalityByBlockHash(client, parameters) { + const { hash } = parameters + try { + const opNodeHttp = http(client.chain.rpcUrls.opNode.http[0]) + const transport = opNodeHttp({}) + const response = await transport.request({ + method: "optimism_btcFinalityByBlockHash", + params: [hash], + }) + // @ts-ignore ts(18046) + return response.length ? response[0] : response + } catch (err) { + if (err.code !== -32000) { + throw err + } + return { + l2_keystone: null, + btc_pub_height: -1, + btc_pub_header_hash: "", + btc_finality: -9, + } + } +} diff --git a/src/chains/hemi-sepolia.js b/src/chains/hemi-sepolia.js new file mode 100644 index 0000000..ccc497c --- /dev/null +++ b/src/chains/hemi-sepolia.js @@ -0,0 +1,53 @@ +import { defineChain } from "viem" +import { chainConfig } from "viem/op-stack" + +const sourceId = 11155111 // Sepolia + +export const hemiSepolia = defineChain({ + ...chainConfig, + id: 743111, + name: "Hemi Sepolia", + nativeCurrency: { + decimals: 18, + name: "Testnet Hemi Ether", + symbol: "ETH", + }, + rpcUrls: { + default: { + http: ["https://testnet.rpc.hemi.network/rpc"], + webSocket: ["wss://testnet.rpc.hemi.network/wsrpc"], + }, + opNode: { + http: ["https://testnet.rpc.hemi.network/noderpc"], + }, + }, + blockExplorers: { + default: { + name: "Hemi Sepolia Explorer", + url: "https://testnet.explorer.hemi.xyz", + }, + }, + contracts: { + l1StandardBridge: { + [sourceId]: { + address: "0xc94b1BEe63A3e101FE5F71C80F912b4F4b055925", + }, + }, + l2OutputOracle: { + [sourceId]: { + address: "0x032d1e1dd960A4B027a9a35FF8B2b672E333Bc27", + }, + }, + multicall3: { + address: "0xcA11bde05977b3631167028862bE2a173976CA11", + blockCreated: 556815, + }, + portal: { + [sourceId]: { + address: "0xB6f9579980aE46f61217A99145645341E49E2516", + }, + }, + }, + testnet: true, + sourceId, +}) diff --git a/src/contracts/bitcoin-kit-txs.js b/src/contracts/bitcoin-kit-txs.js new file mode 100644 index 0000000..f2736d5 --- /dev/null +++ b/src/contracts/bitcoin-kit-txs.js @@ -0,0 +1,385 @@ +import { hemiSepolia } from "../chains/hemi-sepolia.js" + +export const bitcoinKitTxAddresses = { + [hemiSepolia.id]: "0x181dBA19ce25bbD6d884347d2471FE5E5C0fcA4c", +} + +export const bitcoinKitTxsAbi = [ + { + inputs: [ + { + internalType: "string", + name: "btcAddress", + type: "string", + }, + ], + name: "getBitcoinAddressBalance", + outputs: [ + { + internalType: "uint256", + name: "balance", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint32", + name: "height", + type: "uint32", + }, + ], + name: "getHeaderN", + outputs: [ + { + components: [ + { + internalType: "uint32", + name: "height", + type: "uint32", + }, + { + internalType: "bytes32", + name: "blockHash", + type: "bytes32", + }, + { + internalType: "uint32", + name: "version", + type: "uint32", + }, + { + internalType: "bytes32", + name: "previousBlockHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "merkleRoot", + type: "bytes32", + }, + { + internalType: "uint32", + name: "timestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "bits", + type: "uint32", + }, + { + internalType: "uint32", + name: "nonce", + type: "uint32", + }, + ], + internalType: "struct BitcoinHeader", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLastHeader", + outputs: [ + { + components: [ + { + internalType: "uint32", + name: "height", + type: "uint32", + }, + { + internalType: "bytes32", + name: "blockHash", + type: "bytes32", + }, + { + internalType: "uint32", + name: "version", + type: "uint32", + }, + { + internalType: "bytes32", + name: "previousBlockHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "merkleRoot", + type: "bytes32", + }, + { + internalType: "uint32", + name: "timestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "bits", + type: "uint32", + }, + { + internalType: "uint32", + name: "nonce", + type: "uint32", + }, + ], + internalType: "struct BitcoinHeader", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "txId", + type: "bytes32", + }, + ], + name: "getTransactionByTxId", + outputs: [ + { + components: [ + { + internalType: "bytes32", + name: "containingBlockHash", + type: "bytes32", + }, + { + internalType: "uint256", + name: "transactionVersion", + type: "uint256", + }, + { + internalType: "uint256", + name: "size", + type: "uint256", + }, + { + internalType: "uint256", + name: "vSize", + type: "uint256", + }, + { + internalType: "uint256", + name: "lockTime", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "inValue", + type: "uint256", + }, + { + internalType: "bytes32", + name: "inputTxId", + type: "bytes32", + }, + { + internalType: "uint256", + name: "sourceIndex", + type: "uint256", + }, + { + internalType: "bytes", + name: "scriptSig", + type: "bytes", + }, + { + internalType: "uint256", + name: "sequence", + type: "uint256", + }, + { + internalType: "uint256", + name: "fullScriptSigLength", + type: "uint256", + }, + { + internalType: "bool", + name: "containsFullScriptSig", + type: "bool", + }, + ], + internalType: "struct Input[]", + name: "inputs", + type: "tuple[]", + }, + { + components: [ + { + internalType: "uint256", + name: "outValue", + type: "uint256", + }, + { + internalType: "bytes", + name: "script", + type: "bytes", + }, + { + internalType: "string", + name: "outputAddress", + type: "string", + }, + { + internalType: "bool", + name: "isOpReturn", + type: "bool", + }, + { + internalType: "bytes", + name: "opReturnData", + type: "bytes", + }, + { + internalType: "bool", + name: "isSpent", + type: "bool", + }, + { + internalType: "uint256", + name: "fullScriptLength", + type: "uint256", + }, + { + internalType: "bool", + name: "containsFullScript", + type: "bool", + }, + { + components: [ + { + internalType: "bytes32", + name: "spendingTxId", + type: "bytes32", + }, + { + internalType: "uint256", + name: "inputIndex", + type: "uint256", + }, + ], + internalType: "struct SpentDetail", + name: "spentDetail", + type: "tuple", + }, + ], + internalType: "struct Output[]", + name: "outputs", + type: "tuple[]", + }, + { + internalType: "uint256", + name: "totalInputs", + type: "uint256", + }, + { + internalType: "uint256", + name: "totalOutputs", + type: "uint256", + }, + { + internalType: "bool", + name: "containsAllInputs", + type: "bool", + }, + { + internalType: "bool", + name: "containsAllOutputs", + type: "bool", + }, + ], + internalType: "struct Transaction", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "txId", + type: "bytes32", + }, + ], + name: "getTxConfirmations", + outputs: [ + { + internalType: "uint32", + name: "confirmations", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "btcAddress", + type: "string", + }, + { + internalType: "uint256", + name: "pageNumber", + type: "uint256", + }, + { + internalType: "uint256", + name: "pageSize", + type: "uint256", + }, + ], + name: "getUTXOsForBitcoinAddress", + outputs: [ + { + components: [ + { + internalType: "bytes32", + name: "txId", + type: "bytes32", + }, + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "bytes", + name: "scriptPubKey", + type: "bytes", + }, + ], + internalType: "struct UTXO[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, +] diff --git a/src/decorators/public-bitcoin-kit-actions.js b/src/decorators/public-bitcoin-kit-actions.js new file mode 100644 index 0000000..5ade5cc --- /dev/null +++ b/src/decorators/public-bitcoin-kit-actions.js @@ -0,0 +1,25 @@ +import { + getBitcoinAddressBalance, + getHeaderN, + getLastHeader, + getTransactionByTxId, + getTxConfirmations, + getUtxosForBitcoinAddress, +} from "../actions/bitcoin-kit.js" + +export const hemiPublicBitcoinKitActions = function () { + return function (client) { + return { + getBitcoinAddressBalance: (parameters) => + getBitcoinAddressBalance(client, parameters), + getHeaderN: (parameters) => getHeaderN(client, parameters), + getLastHeader: () => getLastHeader(client), + getTransactionByTxId: (parameters) => + getTransactionByTxId(client, parameters), + getTxConfirmations: (parameters) => + getTxConfirmations(client, parameters), + getUtxosForBitcoinAddress: (parameters) => + getUtxosForBitcoinAddress(client, parameters), + } + } +} diff --git a/src/decorators/public-op-node-actions.js b/src/decorators/public-op-node-actions.js new file mode 100644 index 0000000..c0663f8 --- /dev/null +++ b/src/decorators/public-op-node-actions.js @@ -0,0 +1,10 @@ +import { getBtcFinalityByBlockHash } from "../actions/get-btc-finality-by-block-hash.js" + +export const hemiPublicOpNodeActions = function () { + return function (client) { + return { + getBtcFinalityByBlockHash: (parameters) => + getBtcFinalityByBlockHash(client, parameters), + } + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..4ad0549 --- /dev/null +++ b/src/index.js @@ -0,0 +1,14 @@ +export { hemiSepolia } from "./chains/hemi-sepolia.js" + +export { hemiPublicOpNodeActions } from "./decorators/public-op-node-actions.js" +export { getBtcFinalityByBlockHash } from "./actions/get-btc-finality-by-block-hash.js" + +export { hemiPublicBitcoinKitActions } from "./decorators/public-bitcoin-kit-actions.js" +export { + getBitcoinAddressBalance, + getHeaderN, + getLastHeader, + getTransactionByTxId, + getTxConfirmations, + getUtxosForBitcoinAddress, +} from "./actions/bitcoin-kit.js"