├── .env ├── .env.example ├── .github └── workflows │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── atomicals.jpg ├── gulpfile.js ├── lib ├── api │ ├── electrum-api-mock.ts │ ├── electrum-api.interface.ts │ └── electrum-api.ts ├── browser │ └── demoinclude.html ├── cli.ts ├── commands │ ├── address-history-command.ts │ ├── address-info-command.ts │ ├── await-utxo-command.ts │ ├── broadcast-command.ts │ ├── command-helpers.ts │ ├── command-result.interface.ts │ ├── command.interface.ts │ ├── create-dmint-command.ts │ ├── create-dmint-manifest-command.ts │ ├── custom-color-interactive-command.ts │ ├── decode-tx-command.ts │ ├── delete-interactive-command.ts │ ├── disable-subrealm-rules-command.ts │ ├── download-command.ts │ ├── emit-interactive-command.ts │ ├── enable-subrealm-rules-command.ts │ ├── get-atomicals-address-command.ts │ ├── get-atomicals-at-location-command.ts │ ├── get-by-container-command.ts │ ├── get-by-realm-command.ts │ ├── get-by-ticker-command.ts │ ├── get-command.ts │ ├── get-container-item-validated-by-manifest-command.ts │ ├── get-container-item-validated-command.ts │ ├── get-container-item.ts │ ├── get-container-items-command.ts │ ├── get-dft-info-command.ts │ ├── get-global-command.ts │ ├── get-subrealm-info-command.ts │ ├── get-utxos.ts │ ├── init-interactive-dft-command.ts │ ├── init-interactive-fixed-dft-command.ts │ ├── init-interactive-infinite-dft-command.ts │ ├── list-command.ts │ ├── merge-interactive-utxos.ts │ ├── mint-interactive-container-command.ts │ ├── mint-interactive-dat-command.ts │ ├── mint-interactive-dft-command.ts │ ├── mint-interactive-ditem-command.ts │ ├── mint-interactive-ft-command.ts │ ├── mint-interactive-nft-command.ts │ ├── mint-interactive-realm-command.ts │ ├── mint-interactive-subrealm-command.ts │ ├── mint-interactive-subrealm-direct-command.ts │ ├── mint-interactive-subrealm-with-rules-command.ts │ ├── pending-subrealms-command.ts │ ├── render-previews-command.ts │ ├── resolve-command.ts │ ├── seal-interactive-command.ts │ ├── search-containers-command.ts │ ├── search-realms-command.ts │ ├── search-tickers-command.ts │ ├── server-version-command.ts │ ├── set-container-data-interactive-command.ts │ ├── set-container-dmint-interactive-command.ts │ ├── set-interactive-command.ts │ ├── set-relation-interactive-command.ts │ ├── splat-interactive-command.ts │ ├── split-interactive-command.ts │ ├── summary-containers-command.ts │ ├── summary-realms-command.ts │ ├── summary-subrealms-command.ts │ ├── summary-tickers-command.ts │ ├── transfer-interactive-builder-command.ts │ ├── transfer-interactive-ft-command.ts │ ├── transfer-interactive-nft-command.ts │ ├── transfer-interactive-utxos-command.ts │ ├── tx-command.ts │ ├── wallet-create-command.ts │ ├── wallet-import-command.ts │ ├── wallet-info-command.ts │ ├── wallet-init-command.ts │ ├── wallet-phrase-decode-command.ts │ └── witness_stack_to_script_witness.ts ├── errors │ └── Errors.ts ├── index.ts ├── interfaces │ ├── api.interface.ts │ ├── atomical-file-data.ts │ ├── atomical-status.interface.ts │ ├── configuration.interface.ts │ └── filemap.interface.ts ├── types │ ├── UTXO.interface.ts │ └── protocol-tags.ts └── utils │ ├── address-helpers.ts │ ├── address-keypair-path.ts │ ├── atomical-format-helpers.ts │ ├── atomical-operation-builder.ts │ ├── container-validator.ts │ ├── create-key-pair.ts │ ├── create-mnemonic-phrase.ts │ ├── decode-mnemonic-phrase.ts │ ├── file-utils.ts │ ├── hydrate-config.ts │ ├── miner-worker.ts │ ├── prompt-helpers.ts │ ├── select-funding-utxo.ts │ ├── utils.ts │ ├── validate-cli-inputs.ts │ ├── validate-wallet-storage.ts │ └── wallet-path-resolver.ts ├── package.json ├── patches └── bitcoinjs-lib+6.1.3.patch ├── pnpm-lock.yaml ├── templates ├── containers │ ├── dmint-collection-general-metadata.json │ ├── dmint-sample-complex.json │ ├── dmint-sample.json │ ├── full-collection-data.json │ └── minimum-collection-data.json ├── data │ ├── delete1.json │ ├── delete2.json │ ├── delete3.json │ ├── update1.json │ └── update2.json ├── fungible-tokens │ ├── DISCLAIMER.md │ ├── full-ft-meta.json │ ├── minimum-ft-data.json │ ├── sample.json │ └── terms.md ├── socialfi │ ├── base-profile.json │ ├── min-profile.json │ └── readme.md └── subrealms │ ├── sample-rules-arc.json │ └── sample-rules.json ├── test ├── commands │ └── wallet-create.test.js ├── mocha.opts └── utils │ ├── atomical-format-helpers.test.js │ ├── atomical_decorator.test.js │ ├── create-key-pair.test.js │ ├── create-mnemonic-phrase.test.js │ └── script_helpers.test.js ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | ELECTRUMX_PROXY_BASE_URL= 2 | WALLET_PATH=./wallets 3 | WALLET_FILE=wallet.tokens.json 4 | # testnet or livenet or regtest 5 | NETWORK=livenet 6 | 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Bitcoin: 2 | # ELECTRUMX_PROXY_BASE_URL=https://epproxy.your-atomicals-electrumx-indexer/proxy 3 | # 4 | ELECTRUMX_PROXY_BASE_URL= 5 | WALLET_PATH=./wallets 6 | WALLET_FILE=wallet.json 7 | # testnet or livenet or regtest 8 | NETWORK=livenet 9 | CONCURRENCY=4 -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - 'main' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [18.x, 20.x] 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: Setup yarn 22 | run: npm install -g yarn 23 | - name: Use Node.js ${{ matrix.node-version }} with yarn caching 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: yarn 28 | - name: Install dependencies 29 | run: yarn 30 | - name: Build 31 | run: yarn build 32 | - name: Run tests 33 | run: yarn test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .DS_Store 3 | node_modules/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/e2e/reports/ 8 | selenium-debug.log 9 | reveal_txs/ 10 | update_txs/ 11 | payment_txs/ 12 | download_txs/ 13 | transfer_txs/ 14 | wallets/ 15 | wallets/* 16 | wallet.json 17 | wallet.json.* 18 | keypairs.json 19 | owners.json 20 | commit-command-output*.json 21 | # Editor directories and files 22 | .idea 23 | .vscode 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | .gitkeep 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | ############## Stage 1: Build the code using a full node image ################ 3 | ############################################################################### 4 | 5 | FROM node:20.2-alpine AS build 6 | 7 | WORKDIR /app 8 | 9 | COPY . . 10 | 11 | RUN yarn install --frozen-lockfile --network-timeout 1800000 12 | RUN yarn build 13 | 14 | ############################################################################### 15 | ########## Stage 2: Copy over the built code to a leaner image ################ 16 | ############################################################################### 17 | 18 | FROM node:20.2-alpine 19 | 20 | WORKDIR /app 21 | RUN mkdir /app/wallets 22 | 23 | COPY --from=build /app/.env /app/.env 24 | COPY --from=build /app/dist /app/dist 25 | COPY --from=build /app/node_modules /app/node_modules 26 | 27 | ENTRYPOINT ["node", "dist/cli.js"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 The Atomicals Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atomicals Command-line Tool 2 | 3 | The command-line tool to fetch, deploy, mint, transfer, and manipulate Atomicals Digital Assets. 4 | 5 | Visit the [Atomicals Guidebook](https://atomicals-community.github.io/atomicals-guide/) to get to know about Atomicals! 6 | 7 | > Multiple templates are covered for setting up Fungible-tokens, NFT collections, Realm & Sub-realm, and Social-FI! 8 | > Check them out at https://github.com/atomicals/atomicals-js/tree/main/templates. 9 | 10 | **Table Of Contents** 11 | 12 | * [Atomicals Command-line Tool](#atomicals-command-line-tool) 13 | * [Other Atomicals Tools](#other-atomicals-tools) 14 | * [Install](#install) 15 | * [Quick Start](#quick-start) 16 | * [0. Environment File (.env)](#0-environment-file-env) 17 | * [1. Wallet Setup](#1-wallet-setup) 18 | * [2. Explore the CLI](#2-explore-the-cli) 19 | 20 | 21 | ### Other Atomicals Tools 22 | 23 | - Atomicals ElectrumX Indexer Server (https://github.com/atomicals/atomicals-electrumx) 24 | 25 | ### Install 26 | 27 | Use `yarn` (or `pnpm`) package manager instead of `npm`. 28 | 29 | ```shell 30 | # Download the GitHub repo: 31 | git clone https://github.com/atomicals/atomicals-js.git 32 | 33 | cd atomicals-js 34 | 35 | # Build: If you don't have yarn & node installed 36 | # npm install -g node 37 | # npm install -g yarn 38 | 39 | yarn install 40 | yarn run build 41 | 42 | # See all commands at: 43 | yarn run cli --help 44 | ``` 45 | 46 | ### Quick Start 47 | 48 | First, install packages and build, then follow the steps here to create your first Atomical and query the status. 49 | Use `yarn cli`to get a list of all commands available. 50 | 51 | #### 0. Environment File (.env) 52 | 53 | The environment file comes with defaults (`.env.example`), but it is highly recommended to install and operate your own 54 | ElectrumX server. Web browser communication is possible through the `wss` (secure websockets) interface of ElectrumX. 55 | 56 | ```dotenv 57 | ELECTRUMX_PROXY_BASE_URL=https://ep.your-atomicals-electrumx-host/proxy 58 | 59 | # Optional 60 | WALLET_PATH=./wallets 61 | WALLET_FILE=wallet.json 62 | 63 | # The number of concurrent processes to be used. This should not exceed the number of CPU cores available. 64 | # If not set, the default behavior is to use all available CPU cores minus one. 65 | CONCURRENCY=4 66 | ``` 67 | 68 | #### 1. Wallet Setup 69 | 70 | The purpose of the wallet is to create p2tr (pay-to-taproot) spend scripts and to receive change from the transactions 71 | made for the various operations. 72 | _Do not put more funds than you can afford to lose, as this is still beta!_ 73 | 74 | To initialize a new `wallet.json` file that will store your address for receiving change use the `wallet-init` command. 75 | Alternatively, you may populate the `wallet.json` manually, ensuring that the address at `m/86'/0'/0'/0/0` 76 | equals the address and the `derivePath` is set correctly. 77 | 78 | Configure the path in the environment `.env` file to point to your wallet file. defaults to `./wallet.json` 79 | 80 | Default: 81 | 82 | ```dotenv 83 | WALLET_PATH=. 84 | WALLET_FILE=wallet.json 85 | ``` 86 | 87 | Update to `wallets/` directory: 88 | 89 | ```dotenv 90 | WALLET_PATH=./wallets 91 | WALLET_FILE=wallet.json 92 | ``` 93 | 94 | Create the wallet: 95 | 96 | ```shell 97 | yarn cli wallet-init 98 | 99 | >>> 100 | 101 | Wallet created at wallet.json 102 | phrase: maple maple maple maple maple maple maple maple maple maple maple maple 103 | Legacy address (for change): 1FXL2CJ9nAC...u3e9Evdsa2pKrPhkag 104 | Derive Path: m/86'/0'/0'/0/0 105 | WIF: L5Sa65gNR6QsBjqK.....r6o4YzcqNRnJ1p4a6GPxqQQ 106 | ------------------------------------------------------ 107 | ``` 108 | 109 | #### 2. Explore the CLI 110 | 111 | Get all the commands available: 112 | 113 | ```shell 114 | yarn cli --help 115 | ``` 116 | -------------------------------------------------------------------------------- /atomicals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicals/atomicals-js/1333565efbfe5ca4bdb8443a94d72e9f8534c2c4/atomicals.jpg -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 2 | // using data from package.json 3 | var pkg = require('./package.json'); 4 | var source = require('vinyl-source-stream'); 5 | var banner = ['/**', 6 | ' * <%= pkg.name %> - <%= pkg.description %>', 7 | ' * @version v<%= pkg.version %>', 8 | ' * @link <%= pkg.homepage %>', 9 | ' *', 10 | ' * Copyright (c) 2024 The Atomicals Developers', 11 | ' *', 12 | ' * This program is free software: you can redistribute it and/or modify', 13 | ' * it under the terms of the GNU General Public License as published by', 14 | ' * the Free Software Foundation, either version 3 of the License, or', 15 | ' * (at your option) any later version.', 16 | ' * ', 17 | ' * This program is distributed in the hope that it will be useful,', 18 | ' * but WITHOUT ANY WARRANTY; without even the implied warranty of', 19 | ' * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the', 20 | ' * GNU General Public License for more details.', 21 | ' * ', 22 | ' * You should have received a copy of the GNU General Public License', 23 | ' * along with this program. If not, see .', 24 | ' */', 25 | ''].join('\n'); 26 | 27 | var gulp = require('gulp'); 28 | var ts = require('gulp-typescript'); 29 | var header = require('gulp-header'); 30 | var rename = require("gulp-rename"); 31 | 32 | var uglify = require('gulp-uglify'); 33 | 34 | // Load plugins 35 | 36 | gulp.task('build', function () { 37 | return gulp.src('lib/**/*.ts') 38 | .pipe(ts({ 39 | noImplicitAny: false, 40 | // outFile: 'atomicals.js', 41 | // module: 'amd' 42 | })) 43 | .pipe(header(banner, { pkg : pkg } )) 44 | .pipe(uglify()) 45 | .pipe(rename("atomicals.js")) 46 | .pipe(gulp.dest('dist')); 47 | }); 48 | -------------------------------------------------------------------------------- /lib/api/electrum-api.interface.ts: -------------------------------------------------------------------------------- 1 | import { UTXO } from "../types/UTXO.interface"; 2 | 3 | export interface IUnspentResponse { 4 | confirmed: number; 5 | unconfirmed: number; 6 | balance: number; 7 | utxos: UTXO[]; 8 | } 9 | 10 | export interface ElectrumApiInterface { 11 | close: () => Promise; 12 | open: () => Promise; 13 | resetConnection: () => Promise; 14 | isOpen: () => boolean; 15 | sendTransaction: (rawtx: string) => Promise; 16 | getUnspentAddress: (address: string) => Promise; 17 | getUnspentScripthash: (address: string) => Promise; 18 | waitUntilUTXO: (address: string, satoshis: number, sleepTimeSec: number, exactSatoshiAmount?: boolean) => Promise; 19 | getTx: (txid: string) => Promise; 20 | serverVersion: () => Promise; 21 | broadcast: (rawtx: string, force?: boolean) => Promise; 22 | history: (scripthash: string) => Promise; 23 | dump: () => Promise; 24 | estimateFee: (blocks: number) => Promise; 25 | // Atomicals API 26 | atomicalsGetGlobal: (hashes: number) => Promise; 27 | atomicalsGet: (atomicalAliasOrId: string | number) => Promise; 28 | atomicalsGetFtInfo: (atomicalAliasOrId: string | number) => Promise; 29 | atomicalsGetLocation: (atomicalAliasOrId: string | number) => Promise; 30 | atomicalsGetState: (atomicalAliasOrId: string | number) => Promise; 31 | atomicalsGetStateHistory: (atomicalAliasOrId: string | number) => Promise; 32 | atomicalsGetEventHistory: (atomicalAliasOrId: string | number) => Promise; 33 | atomicalsGetTxHistory: (atomicalAliasOrId: string | number) => Promise; 34 | atomicalsList: (limit: number, offset: number, asc: boolean) => Promise; 35 | atomicalsByScripthash: (scripthash: string, verbose?: boolean) => Promise; 36 | atomicalsByAddress: (address: string) => Promise; 37 | atomicalsAtLocation: (location: string) => Promise; 38 | atomicalsGetByContainerItem: (container: string, item: string) => Promise; 39 | atomicalsGetByContainerItemValidated: (container: string, item: string, bitworkc: string, bitworkr: string, main: string, mainHash: string, proof: any, checkWithoutSealed: boolean) => Promise; 40 | atomicalsGetByRealm: (realm: string) => Promise; 41 | atomicalsGetRealmInfo: (realmOrSubRealm: string) => Promise; 42 | atomicalsGetByTicker: (ticker: string) => Promise; 43 | atomicalsGetByContainer: (container: string) => Promise; 44 | atomicalsGetContainerItems: (container: string, limit: number, offset: number) => Promise; 45 | atomicalsFindTickers: (tickerPrefix: string | null, asc?: boolean) => Promise; 46 | atomicalsFindContainers: (containerPrefix: string | null, asc?: boolean) => Promise; 47 | atomicalsFindRealms: (realmPrefix: string | null, asc?: boolean) => Promise; 48 | atomicalsFindSubRealms: (parentRealmId: string, subrealmPrefix: string | null, mostRecentFirst?: boolean) => Promise; 49 | } 50 | -------------------------------------------------------------------------------- /lib/browser/demoinclude.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SDK Example 7 | 8 | 9 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/commands/address-history-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | 5 | export class AddressHistoryCommand implements CommandInterface { 6 | constructor( 7 | private electrumApi: ElectrumApiInterface, 8 | private address: string 9 | ) { 10 | } 11 | 12 | async run(): Promise { 13 | const { scripthash } = detectAddressTypeToScripthash(this.address); 14 | return { 15 | success: true, 16 | data: { 17 | address: this.address, 18 | scripthash: scripthash, 19 | history: await this.electrumApi.history(scripthash) 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /lib/commands/address-info-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | export class AddressInfoCommand implements CommandInterface { 5 | constructor( 6 | private electrumApi: ElectrumApiInterface, 7 | private address: string, 8 | private verbose: boolean 9 | ) { 10 | } 11 | 12 | async run(): Promise { 13 | const { scripthash } = detectAddressTypeToScripthash(this.address); 14 | const balanceInfo = await this.electrumApi.getUnspentScripthash(scripthash); 15 | const res = await this.electrumApi.atomicalsByScripthash(scripthash); 16 | 17 | let history = undefined; 18 | if (this.verbose) { 19 | history = await this.electrumApi.history(scripthash); 20 | } 21 | 22 | // Filter out the utxos that contain atomicals for display the atomicals section 23 | const filteredAtomicalsUtxos: any = []; 24 | const nonAtomicalsBalanceInfoUtxos: any = []; 25 | let nonAtomicalsBalanceInfoConfirmed = 0; 26 | let nonAtomicalsBalanceInfoUnconfirmed = 0; 27 | for (const utxo of res.utxos) { 28 | if (utxo.atomicals && utxo.atomicals.length) { 29 | filteredAtomicalsUtxos.push({ 30 | txid: utxo.txid, 31 | index: utxo.index, 32 | value: utxo.value, 33 | height: utxo.height, 34 | atomicals: utxo.atomicals, 35 | }) 36 | } else if (!utxo.atomicals || !utxo.atomicals.length) { 37 | nonAtomicalsBalanceInfoUtxos.push({ 38 | txid: utxo.txid, 39 | index: utxo.index, 40 | value: utxo.value, 41 | height: utxo.height 42 | }) 43 | if (utxo.height && utxo.height > 0) { 44 | nonAtomicalsBalanceInfoConfirmed += utxo.value 45 | } else { 46 | nonAtomicalsBalanceInfoUnconfirmed += utxo.value 47 | } 48 | 49 | } 50 | } 51 | return { 52 | success: true, 53 | data: { 54 | address: this.address, 55 | scripthash: scripthash, 56 | atomicals: { 57 | count: Object.keys(res.atomicals).length, 58 | balances: res.atomicals, 59 | utxos: filteredAtomicalsUtxos, 60 | }, 61 | globalBalanceInfo: { 62 | unconfirmed: balanceInfo.unconfirmed, 63 | confirmed: balanceInfo.confirmed, 64 | utxos: balanceInfo.utxos 65 | }, 66 | nonAtomicalsBalanceInfo: { 67 | unconfirmed: nonAtomicalsBalanceInfoUnconfirmed, 68 | confirmed: nonAtomicalsBalanceInfoConfirmed, 69 | utxos: nonAtomicalsBalanceInfoUtxos 70 | }, 71 | history 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /lib/commands/await-utxo-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | 4 | export class AwaitUtxoCommand implements CommandInterface { 5 | constructor( 6 | private electrumApi: ElectrumApiInterface, 7 | private address: string, 8 | private amount: number, 9 | ) { 10 | } 11 | async run(): Promise { 12 | const result = await this.electrumApi.waitUntilUTXO(this.address, this.amount, 5); 13 | return { 14 | success: true, 15 | data: result, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/commands/broadcast-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | 4 | export class BroadcastCommand implements CommandInterface { 5 | constructor( 6 | private electrumApi: ElectrumApiInterface, 7 | private rawtx: string, 8 | ) { 9 | } 10 | async run(): Promise { 11 | const result = await this.electrumApi.broadcast(this.rawtx); 12 | return { 13 | success: true, 14 | data: result, 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/commands/command-result.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CommandResultInterface { 2 | success: boolean; 3 | message?: string; 4 | stack?: any; 5 | data?: any; 6 | error?: any; 7 | } 8 | -------------------------------------------------------------------------------- /lib/commands/command.interface.ts: -------------------------------------------------------------------------------- 1 | import { CommandResultInterface } from "./command-result.interface"; 2 | 3 | export enum AtomicalsGetFetchType { 4 | GET = 'GET', 5 | LOCATION = 'LOCATION', 6 | STATE = 'STATE', 7 | STATE_HISTORY = 'STATE_HISTORY', 8 | EVENT_HISTORY = 'EVENT_HISTORY', 9 | TX_HISTORY = 'TX_HISTORY' 10 | } 11 | export interface CommandInterface { 12 | run: () => Promise; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /lib/commands/create-dmint-manifest-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as cloneDeep from 'lodash.clonedeep'; 4 | import { BitworkInfo, buildAtomicalsFileMapFromRawTx, getTxIdFromAtomicalId, hexifyObjectWithUtf8, isValidBitworkString, isValidDmitemName } from "../utils/atomical-format-helpers"; 5 | import { fileWriter, jsonFileReader, jsonFileWriter } from "../utils/file-utils"; 6 | import * as fs from 'fs'; 7 | import * as path from 'path' 8 | import * as mime from 'mime-types'; 9 | import { FileMap } from "../interfaces/filemap.interface"; 10 | import { basename, extname } from "path"; 11 | import { hash256 } from 'bitcoinjs-lib/src/crypto'; 12 | import { MerkleTree } from 'merkletreejs' 13 | const SHA256 = require('crypto-js/sha256') 14 | function isInvalidImageExtension(extName) { 15 | return extName !== '.jpg' && extName !== '.gif' && extName !== '.jpeg' && extName !== '.png' && extName !== '.svg' && extName !== '.webp' && 16 | extName !== '.mp3' && extName !== '.mp4' && extName !== '.mov' && extName !== '.webm' && extName !== '.avi' && extName !== '.mpg' 17 | } 18 | 19 | // Skip folders and invalid files under this folder 20 | function isInvalidFile(file, folder) { 21 | const filePath = path.join(folder, file); 22 | const stats = fs.statSync(filePath); 23 | 24 | // Skip any folder 25 | if (stats.isDirectory()) { 26 | console.log(`Skipping ${file}...`); 27 | return true; 28 | } 29 | 30 | // Skip any file whose name starts with . 31 | if (file.startsWith('.')) { 32 | console.log(`Skipping ${file}...`); 33 | return true; 34 | } 35 | 36 | return false 37 | } 38 | 39 | export class CreateDmintItemManifestsCommand implements CommandInterface { 40 | constructor( 41 | private folder: string, 42 | private outputName: string, 43 | ) { 44 | } 45 | async run(): Promise { 46 | // Read the folder for any images 47 | let counter = 0; 48 | const files = fs.readdirSync(this.folder); 49 | const filemap = {}; 50 | const leafItems: any = []; 51 | for (const file of files) { 52 | if (isInvalidFile(file, this.folder)) { 53 | continue; 54 | } 55 | 56 | const basePath = basename(file); 57 | const extName = extname(file); 58 | const splitBase = basePath.split('.'); 59 | 60 | if (splitBase.length !== 2) { 61 | throw new Error('Image file must have exactly with dot extension: ' + basePath) 62 | } 63 | const rawName = splitBase[0]; 64 | if (isInvalidImageExtension(extName)) { 65 | continue; 66 | } 67 | isValidDmitemName(rawName); 68 | filemap[rawName] = filemap[rawName] || {} 69 | const fileBuf = fs.readFileSync(this.folder + '/' + file); 70 | const hashed = hash256(fileBuf); 71 | const hashedStr = hashed.toString('hex'); 72 | console.log(`Generating hashes for filename ${basePath} with hash ${hashedStr}`); 73 | const filename = 'image' + extName; 74 | filemap[rawName][filename] = { 75 | '$b': fileBuf.toString('hex') 76 | } 77 | counter++; 78 | const leafVector = rawName + ':' + filename + ':' + hashedStr; 79 | leafItems.push({ 80 | id: rawName, 81 | filename, 82 | hashedStr, 83 | leafVector, 84 | fileBuf: fileBuf.toString('hex') 85 | }); 86 | }; 87 | const leaves = leafItems.map(x => SHA256(x.leafVector)) 88 | const tree = new MerkleTree(leaves, SHA256) 89 | const root = tree.getRoot().toString('hex') 90 | 91 | for (const leafItem of leafItems) { 92 | const leaf = SHA256(leafItem.leafVector) 93 | const proof = tree.getProof(leaf) 94 | tree.verify(proof, leaf, root) 95 | filemap[leafItem.id]['args'] = { 96 | request_dmitem: leafItem.id, 97 | main: leafItem.filename, 98 | i: true // Default everything to immutable 99 | } 100 | filemap[leafItem.id]['leafVector'] = leafItem.leafVector 101 | filemap[leafItem.id]['hash'] = leafItem.hashedStr 102 | filemap[leafItem.id]['fileBuf'] = leafItem.fileBuf 103 | } 104 | 105 | const timestamp = (new Date()).getTime(); 106 | const dirName = this.outputName + '-' + timestamp; 107 | if (!fs.existsSync(dirName)) { 108 | fs.mkdirSync(dirName); 109 | } 110 | for (const itemProp in filemap) { 111 | if (!filemap.hasOwnProperty(itemProp)) { 112 | continue; 113 | } 114 | await jsonFileWriter(`${dirName}/item-${itemProp}.json`, { 115 | "mainHash": filemap[itemProp].hash, 116 | "data": { 117 | args: { 118 | request_dmitem: itemProp, 119 | main: filemap[itemProp].args.main, 120 | i: filemap[itemProp].args.i, 121 | proof: filemap[itemProp].args.proof 122 | }, 123 | [filemap[itemProp].args.main]: { 124 | '$b': filemap[itemProp].fileBuf 125 | }, 126 | } 127 | }); 128 | } 129 | 130 | return { 131 | success: true, 132 | data: { 133 | folder: this.folder, 134 | totalItems: counter, 135 | } 136 | }; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/commands/custom-color-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | import * as readline from 'readline'; 6 | const bitcoin = require('bitcoinjs-lib'); 7 | bitcoin.initEccLib(ecc); 8 | import { 9 | initEccLib, 10 | } from "bitcoinjs-lib"; 11 | import { logBanner } from "./command-helpers"; 12 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 13 | import { BaseRequestOptions } from "../interfaces/api.interface"; 14 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 15 | import { GetAtomicalsAtLocationCommand } from "./get-atomicals-at-location-command"; 16 | import { GetUtxoPartialFromLocation } from "../utils/address-helpers"; 17 | import { IInputUtxoPartial } from "../types/UTXO.interface"; 18 | import { hasAtomicalType, isAtomicalId } from "../utils/atomical-format-helpers"; 19 | 20 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 21 | initEccLib(tinysecp as any); 22 | 23 | 24 | export class customColorInteractiveCommand implements CommandInterface { 25 | constructor( 26 | private electrumApi: ElectrumApiInterface, 27 | private options: BaseRequestOptions, 28 | private locationId: string, 29 | private owner: IWalletRecord, 30 | private funding: IWalletRecord, 31 | ) { 32 | } 33 | async run(): Promise { 34 | logBanner(`Custom color FTs Interactive`); 35 | const command: CommandInterface = new GetAtomicalsAtLocationCommand(this.electrumApi, this.locationId); 36 | const response: any = await command.run(); 37 | if (!response || !response.success) { 38 | throw new Error(response); 39 | } 40 | const atomicals = response.data.atomicals; 41 | 42 | const hasNfts = hasAtomicalType('NFT', atomicals); 43 | if (hasNfts) { 44 | console.log('Found at least one NFT at the same location. The first output will contain the NFTs, and the second output, etc will contain the FTs split out. After you may use the splat command to separate multiple NFTs if they exist at the same location.') 45 | } 46 | 47 | const inputUtxoPartial: IInputUtxoPartial | any = GetUtxoPartialFromLocation(this.owner.address, response.data.location_info); 48 | const atomicalBuilder = new AtomicalOperationBuilder({ 49 | electrumApi: this.electrumApi, 50 | rbf: this.options.rbf, 51 | satsbyte: this.options.satsbyte, 52 | address: this.owner.address, 53 | disableMiningChalk: this.options.disableMiningChalk, 54 | opType: 'z', 55 | skipOptions: { 56 | }, 57 | meta: this.options.meta, 58 | ctx: this.options.ctx, 59 | init: this.options.init, 60 | }); 61 | 62 | // Add the owner of the atomicals at the location 63 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 64 | const atomicalsToColored: any = {} 65 | let index = 0; 66 | for (const atomical of atomicals) { 67 | if (!atomical.atomical_id) { 68 | throw new Error('Critical error atomical_id not set for FT'); 69 | } 70 | if (!isAtomicalId(atomical.atomical_id)) { 71 | throw new Error('Critical error atomical_id is not valid for FT'); 72 | } 73 | 74 | console.log('-') 75 | console.log(`current atomical_id: ${atomical.atomical_id}`) 76 | 77 | const outputs: {} = await this.promptCustomColored(index, atomical.value); 78 | 79 | // Make sure to make N outputs, for each atomical NFT 80 | atomicalsToColored[atomical.atomical_id] = outputs; 81 | for (const [key, value] of Object.entries(outputs)) { 82 | let atomical_value: number = value as number; 83 | if (atomical_value < 546 && atomical_value > 0) { 84 | atomical_value = 546 85 | } 86 | if (atomical_value == 0) { 87 | continue 88 | } 89 | atomicalBuilder.addOutput({ 90 | address: inputUtxoPartial.address, 91 | value: atomical_value 92 | }); 93 | index += 1; 94 | } 95 | } 96 | await atomicalBuilder.setData(atomicalsToColored); 97 | console.log(atomicalBuilder); 98 | await this.promptContinue(); 99 | const result = await atomicalBuilder.start(this.funding.WIF); 100 | return { 101 | success: true, 102 | data: result 103 | } 104 | } 105 | 106 | async promptCustomColored(index, availableBalance): Promise<{}> { 107 | let remainingBalance = availableBalance; 108 | const atomicalColored = {}; 109 | const rl = readline.createInterface({ 110 | input: process.stdin, 111 | output: process.stdout 112 | }); 113 | 114 | try { 115 | const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); 116 | while (remainingBalance > 0) { 117 | console.log('-') 118 | console.log(`Remaining amount: ${remainingBalance}`) 119 | 120 | const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); 121 | let reply = (await prompt("Current outputs uxto index is " + index + ", Please enter amount you want to separated: ") as any); 122 | 123 | console.log('--------'); 124 | if (reply === 'f') { 125 | break; 126 | } 127 | const valuePart = parseInt(reply, 10); 128 | 129 | if (valuePart < 0 || !valuePart) { 130 | console.log('Invalid value, minimum: 1') 131 | continue; 132 | } 133 | 134 | if (remainingBalance - valuePart < 0) { 135 | console.log('Invalid value, maximum remaining: ' + remainingBalance); 136 | continue; 137 | } 138 | 139 | atomicalColored[index] = valuePart; 140 | remainingBalance -= valuePart; 141 | index += 1; 142 | } 143 | if (remainingBalance > 0) { 144 | throw new Error('Remaining balance was not 0') 145 | } 146 | console.log('Successfully allocated entire available amounts to recipients...') 147 | return atomicalColored; 148 | } finally { 149 | rl.close(); 150 | } 151 | } 152 | 153 | async promptContinue() { 154 | const rl = readline.createInterface({ 155 | input: process.stdin, 156 | output: process.stdout 157 | }); 158 | 159 | try { 160 | let reply: string = ''; 161 | const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); 162 | 163 | reply = (await prompt("Does everything look good above? To continue type 'y' or 'yes': ") as any); 164 | 165 | if (reply === 'y' || reply === 'yes') { 166 | return; 167 | } 168 | throw 'Aborted'; 169 | } finally { 170 | rl.close(); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/commands/decode-tx-command.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CommandInterface } from "./command.interface"; 3 | import { Transaction } from 'bitcoinjs-lib/src/transaction'; 4 | 5 | export class DecodeTxCommand implements CommandInterface { 6 | constructor( 7 | private rawtx: string 8 | ) { 9 | 10 | } 11 | async run(): Promise { 12 | const tx = Transaction.fromHex(this.rawtx); 13 | return { 14 | success: true, 15 | data: { 16 | tx: tx, 17 | } 18 | }; 19 | } 20 | } -------------------------------------------------------------------------------- /lib/commands/delete-interactive-command.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 4 | import { CommandInterface } from "./command.interface"; 5 | import * as ecc from 'tiny-secp256k1'; 6 | import { TinySecp256k1Interface } from 'ecpair'; 7 | const bitcoin = require('bitcoinjs-lib'); 8 | bitcoin.initEccLib(ecc); 9 | import { 10 | initEccLib, 11 | } from "bitcoinjs-lib"; 12 | import { getAndCheckAtomicalInfo, logBanner, prepareFilesDataAsObject, readJsonFileAsCompleteDataObjectEncodeAtomicalIds } from "./command-helpers"; 13 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 14 | import { BaseRequestOptions } from "../interfaces/api.interface"; 15 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 16 | 17 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 18 | initEccLib(tinysecp as any); 19 | 20 | export class DeleteInteractiveCommand implements CommandInterface { 21 | constructor( 22 | private electrumApi: ElectrumApiInterface, 23 | private options: BaseRequestOptions, 24 | private atomicalId: string, 25 | private filesWithDeleteKeys: string, 26 | private owner: IWalletRecord, 27 | private funding: IWalletRecord, 28 | ) { 29 | } 30 | async run(): Promise { 31 | logBanner(`Delete Interactive`); 32 | 33 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.atomicalId, this.owner.address); 34 | 35 | const atomicalBuilder = new AtomicalOperationBuilder({ 36 | electrumApi: this.electrumApi, 37 | rbf: this.options.rbf, 38 | satsbyte: this.options.satsbyte, 39 | address: this.owner.address, 40 | disableMiningChalk: this.options.disableMiningChalk, 41 | opType: 'mod', 42 | nftOptions: { 43 | satsoutput: this.options.satsoutput as any 44 | }, 45 | meta: this.options.meta, 46 | ctx: this.options.ctx, 47 | init: this.options.init, 48 | }); 49 | 50 | let filesData = await readJsonFileAsCompleteDataObjectEncodeAtomicalIds(this.filesWithDeleteKeys); 51 | await atomicalBuilder.setData({ 52 | ...filesData, 53 | $a: 1, 54 | }); 55 | 56 | // Attach any requested bitwork 57 | if (this.options.bitworkc) { 58 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 59 | } 60 | 61 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 62 | 63 | // The receiver output 64 | atomicalBuilder.addOutput({ 65 | address: this.owner.address, 66 | value: this.options.satsoutput as any || 1000 67 | }); 68 | 69 | const result = await atomicalBuilder.start(this.funding.WIF); 70 | return { 71 | success: true, 72 | data: result 73 | } 74 | } 75 | } 76 | 77 | 78 | -------------------------------------------------------------------------------- /lib/commands/disable-subrealm-rules-command.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 4 | import { CommandInterface } from "./command.interface"; 5 | import * as ecc from 'tiny-secp256k1'; 6 | import { TinySecp256k1Interface } from 'ecpair'; 7 | const bitcoin = require('bitcoinjs-lib'); 8 | bitcoin.initEccLib(ecc); 9 | import { 10 | initEccLib, 11 | } from "bitcoinjs-lib"; 12 | import { getAndCheckAtomicalInfo, logBanner } from "./command-helpers"; 13 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 14 | import { BaseRequestOptions } from "../interfaces/api.interface"; 15 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 16 | 17 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 18 | initEccLib(tinysecp as any); 19 | 20 | export class DisableSubrealmRulesInteractiveCommand implements CommandInterface { 21 | constructor( 22 | private electrumApi: ElectrumApiInterface, 23 | private options: BaseRequestOptions, 24 | private atomicalId: string, 25 | private funding: IWalletRecord, 26 | private owner: IWalletRecord, 27 | ) { 28 | } 29 | async run(): Promise { 30 | logBanner(`Disable Subrealm Minting Rules Interactive`); 31 | 32 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.atomicalId, this.owner.address, 'NFT', null); 33 | 34 | const atomicalBuilder = new AtomicalOperationBuilder({ 35 | electrumApi: this.electrumApi, 36 | rbf: this.options.rbf, 37 | satsbyte: this.options.satsbyte, 38 | address: this.owner.address, 39 | disableMiningChalk: this.options.disableMiningChalk, 40 | opType: 'mod', 41 | nftOptions: { 42 | satsoutput: this.options.satsoutput as any 43 | }, 44 | meta: this.options.meta, 45 | ctx: this.options.ctx, 46 | init: this.options.init, 47 | }); 48 | 49 | await atomicalBuilder.setData({ 50 | subrealms: true, 51 | $a: 1 52 | }); 53 | // Just add some bitwork to make it use the funding address 54 | atomicalBuilder.setBitworkCommit('1'); 55 | 56 | // Add the atomical to update 57 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 58 | // The receiver output 59 | atomicalBuilder.addOutput({ 60 | address: this.owner.address, 61 | value: this.options.satsoutput as any || 1000 62 | }); 63 | 64 | 65 | 66 | const result = await atomicalBuilder.start(this.funding.WIF); 67 | return { 68 | success: true, 69 | data: result 70 | } 71 | } 72 | } 73 | 74 | 75 | -------------------------------------------------------------------------------- /lib/commands/download-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as cloneDeep from 'lodash.clonedeep'; 4 | import { buildAtomicalsFileMapFromRawTx, getTxIdFromAtomicalId, hexifyObjectWithUtf8 } from "../utils/atomical-format-helpers"; 5 | import { fileWriter, jsonFileWriter } from "../utils/file-utils"; 6 | import * as fs from 'fs'; 7 | import * as mime from 'mime-types'; 8 | import { FileMap } from "../interfaces/filemap.interface"; 9 | 10 | export const writeFiles = async (inputIndexToFilesMap: any, txDir: string): Promise => { 11 | const fileSummary = {}; 12 | for (const inputIndex in inputIndexToFilesMap) { 13 | if (!inputIndexToFilesMap.hasOwnProperty(inputIndex)) { 14 | continue; 15 | } 16 | const inputTxDir = txDir + `/${inputIndex}`; 17 | if (!fs.existsSync(inputTxDir)) { 18 | fs.mkdirSync(inputTxDir); 19 | } 20 | fileSummary[inputIndex] = { 21 | directory: inputTxDir, 22 | files: {} 23 | } 24 | const rawdata = inputIndexToFilesMap[inputIndex].rawdata; 25 | const rawdataPath = inputTxDir + `/_rawdata.hex` 26 | await fileWriter(rawdataPath, rawdata.toString('hex')); 27 | const decoded = inputIndexToFilesMap[inputIndex]['decoded']; 28 | const fulldecodedPath = inputTxDir + `/_rawdata.json`; 29 | const objectDecoded = Object.assign({}, {}, decoded); 30 | const copiedObjectDecoded = cloneDeep(objectDecoded); 31 | await fileWriter(fulldecodedPath, JSON.stringify(hexifyObjectWithUtf8(copiedObjectDecoded, false), null, 2)); 32 | if (decoded) { 33 | for (const filename in decoded) { 34 | if (!decoded.hasOwnProperty(filename)) { 35 | continue; 36 | } 37 | const fileEntry = decoded[filename]; 38 | if (fileEntry['$ct'] && fileEntry['$b']) { 39 | const contentType = fileEntry['$ct']; 40 | const detectedExtension = mime.extension(contentType) || '.dat'; 41 | let fileNameWithExtension = `${filename}.${detectedExtension}`; 42 | const fullPath = inputTxDir + `/${fileNameWithExtension}` 43 | /* if (/utf8/.test(contentType)) { 44 | await fileWriter(fullPath, fileEntry['d']); 45 | } else { 46 | }*/ 47 | await fileWriter(fullPath, fileEntry['$b']); 48 | const contentLength = fileEntry['$b'].length; 49 | const body = fileEntry['$b']; 50 | fileSummary[inputIndex]['files'][filename] = { 51 | filename, 52 | fileNameWithExtension, 53 | detectedExtension, 54 | fullPath, 55 | contentType, 56 | contentLength, 57 | body: body.toString('hex') 58 | } 59 | } else if (fileEntry['$b']) { 60 | // when there is not explicit content type with 'ct' then assume it is json 61 | const contentType = 'application/json' 62 | const fileNameWithExtension = `${filename}.property.json`; 63 | const fullPath = inputTxDir + `/${fileNameWithExtension}` 64 | await fileWriter(fullPath, JSON.stringify(fileEntry, null, 2)); 65 | const contentLength = fileEntry['$b'].length; 66 | const body = fileEntry['$b']; 67 | fileSummary[inputIndex]['files'][filename] = { 68 | filename, 69 | fileNameWithExtension, 70 | detectedExtension: '.json', 71 | fullPath, 72 | contentType, 73 | contentLength, 74 | body: body.toString('hex') 75 | } 76 | } else { 77 | // when there is not explicit content type with 'ct' then assume it is json 78 | const contentType = 'application/json' 79 | const fileNameWithExtension = `${filename}.property.json`; 80 | const fullPath = inputTxDir + `/${fileNameWithExtension}` 81 | await fileWriter(fullPath, JSON.stringify(fileEntry, null, 2)); 82 | const contentLength = fileEntry.length; 83 | const body = fileEntry 84 | fileSummary[inputIndex]['files'][filename] = { 85 | filename, 86 | fileNameWithExtension, 87 | detectedExtension: '.json', 88 | fullPath, 89 | contentType, 90 | contentLength, 91 | body: body 92 | } 93 | } 94 | } 95 | } 96 | } 97 | await jsonFileWriter(txDir + '/manifest.json', fileSummary); 98 | return fileSummary; 99 | } 100 | export class DownloadCommand implements CommandInterface { 101 | constructor( 102 | private electrumApi: ElectrumApiInterface, 103 | private atomicalIdOrTxId: string, 104 | ) { 105 | } 106 | async run(): Promise { 107 | const txid = getTxIdFromAtomicalId(this.atomicalIdOrTxId); 108 | const txResult = await this.electrumApi.getTx(txid); 109 | 110 | if (!txResult || !txResult.success) { 111 | throw `transaction not found ${txid}`; 112 | } 113 | const tx = txResult.tx; 114 | const downloadDir = `download_txs/`; 115 | if (!fs.existsSync(downloadDir)) { 116 | fs.mkdirSync(downloadDir); 117 | } 118 | const txDir = `${downloadDir}/${txid}`; 119 | if (!fs.existsSync(txDir)) { 120 | fs.mkdirSync(txDir); 121 | } 122 | await fileWriter(txDir + `/${txid}.hex`, tx) 123 | const filemap = buildAtomicalsFileMapFromRawTx(tx, false, false) 124 | const writeResult = await writeFiles(filemap, txDir); 125 | return { 126 | success: true, 127 | data: { 128 | txid, 129 | filemap: writeResult 130 | } 131 | }; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/commands/emit-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { getAndCheckAtomicalInfo, logBanner, prepareFilesDataAsObject } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | 15 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 16 | initEccLib(tinysecp as any); 17 | 18 | export class EmitInteractiveCommand implements CommandInterface { 19 | constructor( 20 | private electrumApi: ElectrumApiInterface, 21 | private options: BaseRequestOptions, 22 | private atomicalId: string, 23 | private files: string[], 24 | private owner: IWalletRecord, 25 | private funding: IWalletRecord, 26 | ) { 27 | 28 | } 29 | async run(): Promise { 30 | logBanner(`Emit Interactive`); 31 | 32 | // Attach any default data 33 | let filesData = await prepareFilesDataAsObject(this.files); 34 | 35 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.atomicalId, this.owner.address, 'NFT'); 36 | const atomicalBuilder = new AtomicalOperationBuilder({ 37 | electrumApi: this.electrumApi, 38 | rbf: this.options.rbf, 39 | satsbyte: this.options.satsbyte, 40 | address: this.owner.address, 41 | disableMiningChalk: this.options.disableMiningChalk, 42 | opType: 'evt', 43 | nftOptions: { 44 | satsoutput: this.options.satsoutput as any 45 | }, 46 | meta: this.options.meta, 47 | ctx: this.options.ctx, 48 | init: this.options.init, 49 | }); 50 | 51 | await atomicalBuilder.setData(filesData); 52 | 53 | // Add the atomical to update 54 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 55 | 56 | // The receiver output 57 | atomicalBuilder.addOutput({ 58 | address: this.owner.address, 59 | value: this.options.satsoutput as any || 1000 60 | }); 61 | 62 | const result = await atomicalBuilder.start(this.funding.WIF); 63 | return { 64 | success: true, 65 | data: result 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /lib/commands/enable-subrealm-rules-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { getAndCheckAtomicalInfo, logBanner, prepareFilesDataAsObject, readJsonFileAsCompleteDataObjectEncodeAtomicalIds } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | import { AtomicalIdentifierType, validateSubrealmRulesObject } from "../utils/atomical-format-helpers"; 15 | 16 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 17 | initEccLib(tinysecp as any); 18 | 19 | export class EnableSubrealmRulesCommand implements CommandInterface { 20 | constructor( 21 | private electrumApi: ElectrumApiInterface, 22 | private options: BaseRequestOptions, 23 | private atomicalId: string, 24 | private file: string, 25 | private funding: IWalletRecord, 26 | private owner: IWalletRecord, 27 | ) { 28 | 29 | } 30 | async run(): Promise { 31 | logBanner(`Enable Subrealm Rules Interactive`); 32 | // Attach any default data 33 | let filesData = await readJsonFileAsCompleteDataObjectEncodeAtomicalIds(this.file, false); 34 | // validateSubrealmRulesObject(filesData); 35 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.atomicalId, this.owner.address, 'NFT', null); 36 | const atomicalBuilder = new AtomicalOperationBuilder({ 37 | electrumApi: this.electrumApi, 38 | rbf: this.options.rbf, 39 | satsbyte: this.options.satsbyte, 40 | address: this.owner.address, 41 | disableMiningChalk: this.options.disableMiningChalk, 42 | opType: 'mod', 43 | nftOptions: { 44 | satsoutput: this.options.satsoutput as any 45 | }, 46 | meta: this.options.meta, 47 | ctx: this.options.ctx, 48 | init: this.options.init, 49 | }); 50 | await atomicalBuilder.setData(filesData); 51 | // Just add some bitwork to make it use the funding address 52 | atomicalBuilder.setBitworkCommit('1'); 53 | // Add the atomical to update 54 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 55 | // The receiver output 56 | atomicalBuilder.addOutput({ 57 | address: this.owner.address, 58 | value: this.options.satsoutput as any || 1000 59 | }); 60 | const result = await atomicalBuilder.start(this.funding.WIF); 61 | return { 62 | success: true, 63 | data: result 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /lib/commands/get-atomicals-address-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | export class GetAtomicalsAddressCommand implements CommandInterface { 5 | constructor( 6 | private electrumApi: ElectrumApiInterface, 7 | private address: string, 8 | ) { 9 | } 10 | async run(): Promise { 11 | const { scripthash } = detectAddressTypeToScripthash(this.address) 12 | const result = await this.electrumApi.atomicalsByScripthash(scripthash); 13 | return result; 14 | } 15 | } -------------------------------------------------------------------------------- /lib/commands/get-atomicals-at-location-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | export class GetAtomicalsAtLocationCommand implements CommandInterface { 4 | constructor( 5 | private electrumApi: ElectrumApiInterface, 6 | private location: string, 7 | ) { 8 | } 9 | async run(): Promise { 10 | return { 11 | success: true, 12 | data: await this.electrumApi.atomicalsAtLocation(this.location) 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /lib/commands/get-by-container-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import { decorateAtomical } from "../utils/atomical-format-helpers"; 4 | import { GetCommand } from "./get-command"; 5 | 6 | export class GetByContainerCommand implements CommandInterface { 7 | 8 | constructor( 9 | private electrumApi: ElectrumApiInterface, 10 | private container: string, 11 | private fetchType: AtomicalsGetFetchType = AtomicalsGetFetchType.GET, 12 | ) { 13 | 14 | } 15 | 16 | async run(): Promise { 17 | const trimmedContainer = this.container.startsWith('#') ? this.container.substring(1) : this.container; 18 | const responseResult = await this.electrumApi.atomicalsGetByContainer(trimmedContainer); 19 | if (!responseResult.result || !responseResult.result.atomical_id) { 20 | return { 21 | success: false, 22 | data: responseResult.result 23 | } 24 | } 25 | const getDefaultCommand = new GetCommand(this.electrumApi, responseResult.result.atomical_id, this.fetchType); 26 | const getDefaultCommandResponse = await getDefaultCommand.run(); 27 | const updatedRes = Object.assign({}, 28 | getDefaultCommandResponse.data, 29 | { 30 | result: decorateAtomical(getDefaultCommandResponse.data.result) 31 | } 32 | ); 33 | return { 34 | success: true, 35 | data: updatedRes 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/commands/get-by-realm-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import { decorateAtomical } from "../utils/atomical-format-helpers"; 4 | import { GetCommand } from "./get-command"; 5 | 6 | export class GetByRealmCommand implements CommandInterface { 7 | 8 | constructor(private electrumApi: ElectrumApiInterface, 9 | private realm: string, 10 | private fetchType: AtomicalsGetFetchType = AtomicalsGetFetchType.GET, 11 | ) { 12 | 13 | } 14 | async run(): Promise { 15 | const responseResult = await this.electrumApi.atomicalsGetRealmInfo(this.realm); 16 | if (!responseResult.result || !responseResult.result.atomical_id) { 17 | return { 18 | success: false, 19 | data: responseResult.result 20 | } 21 | } 22 | const getDefaultCommand = new GetCommand( this.electrumApi, responseResult.result.atomical_id, this.fetchType); 23 | const getDefaultCommandResponse = await getDefaultCommand.run(); 24 | const updatedRes = Object.assign({}, 25 | getDefaultCommandResponse.data, 26 | { 27 | result: decorateAtomical(getDefaultCommandResponse.data.result) 28 | } 29 | ); 30 | return { 31 | success: true, 32 | data: updatedRes 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/commands/get-by-ticker-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import { decorateAtomical } from "../utils/atomical-format-helpers"; 4 | import { GetCommand } from "./get-command"; 5 | 6 | export class GetByTickerCommand implements CommandInterface { 7 | constructor( 8 | private electrumApi: ElectrumApiInterface, 9 | private ticker: string, 10 | private fetchType: AtomicalsGetFetchType = AtomicalsGetFetchType.GET, 11 | ) { 12 | 13 | } 14 | async run(): Promise { 15 | const responseResult = await this.electrumApi.atomicalsGetByTicker(this.ticker); 16 | if (!responseResult.result || !responseResult.result.atomical_id) { 17 | console.log(responseResult); 18 | return { 19 | success: false, 20 | data: responseResult.result 21 | } 22 | } 23 | const getDefaultCommand = new GetCommand(this.electrumApi, responseResult.result.atomical_id, this.fetchType); 24 | const getDefaultCommandResponse = await getDefaultCommand.run(); 25 | const updatedRes = Object.assign({}, 26 | getDefaultCommandResponse.data, 27 | { 28 | result: decorateAtomical(getDefaultCommandResponse.data.result) 29 | } 30 | ); 31 | return { 32 | success: true, 33 | data: updatedRes 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/commands/get-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import { decorateAtomical } from "../utils/atomical-format-helpers"; 4 | 5 | export class GetCommand implements CommandInterface { 6 | constructor(private electrumApi: ElectrumApiInterface, 7 | private atomicalAliasOrId: string, 8 | private fetchType: AtomicalsGetFetchType = AtomicalsGetFetchType.GET, 9 | ) { 10 | } 11 | 12 | async run(): Promise { 13 | let response; 14 | if (this.fetchType === AtomicalsGetFetchType.GET) { 15 | response = await this.electrumApi.atomicalsGet(this.atomicalAliasOrId); 16 | } else if (this.fetchType === AtomicalsGetFetchType.LOCATION) { 17 | response = await this.electrumApi.atomicalsGetLocation(this.atomicalAliasOrId); 18 | } else if (this.fetchType === AtomicalsGetFetchType.STATE) { 19 | response = await this.electrumApi.atomicalsGetState(this.atomicalAliasOrId); 20 | } else if (this.fetchType === AtomicalsGetFetchType.STATE_HISTORY) { 21 | response = await this.electrumApi.atomicalsGetStateHistory(this.atomicalAliasOrId); 22 | } else if (this.fetchType === AtomicalsGetFetchType.EVENT_HISTORY) { 23 | response = await this.electrumApi.atomicalsGetEventHistory(this.atomicalAliasOrId); 24 | } else if (this.fetchType === AtomicalsGetFetchType.TX_HISTORY) { 25 | response = await this.electrumApi.atomicalsGetTxHistory(this.atomicalAliasOrId); 26 | } else { 27 | throw new Error('Invalid AtomicalsGetFetchType'); 28 | } 29 | const updatedRes = Object.assign({}, 30 | response, 31 | { 32 | result: decorateAtomical(response.result) 33 | } 34 | ); 35 | return { 36 | success: true, 37 | data: updatedRes 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /lib/commands/get-container-item-validated-by-manifest-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { AtomicalStatus } from "../interfaces/atomical-status.interface"; 11 | import { GetByContainerCommand } from "./get-by-container-command"; 12 | import { jsonFileReader } from "../utils/file-utils"; 13 | import { GetContainerItemValidatedCommand } from "./get-container-item-validated-command"; 14 | import { hash256 } from "bitcoinjs-lib/src/crypto"; 15 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 16 | initEccLib(tinysecp as any); 17 | export interface ResolvedRealm { 18 | atomical: AtomicalStatus 19 | } 20 | export class GetContainerItemValidatedByManifestCommand implements CommandInterface { 21 | constructor( 22 | private electrumApi: ElectrumApiInterface, 23 | private container: string, 24 | private requestDmitem: string, 25 | private manifestJsonFile: string, 26 | private checkWithoutSealed: boolean, 27 | ) { 28 | this.container = this.container.startsWith('#') ? this.container.substring(1) : this.container; 29 | } 30 | async run(): Promise { 31 | const getCmd = new GetByContainerCommand(this.electrumApi, this.container, AtomicalsGetFetchType.GET); 32 | const getResponse = await getCmd.run(); 33 | if (!getResponse.success || !getResponse.data.result.atomical_id) { 34 | return { 35 | success: false, 36 | msg: 'Error retrieving container parent atomical ' + this.container, 37 | data: getResponse.data 38 | } 39 | } 40 | const parentContainerId = getResponse.data.result.atomical_id; 41 | const errors: string[] = getResponse.data.result.$container_dmint_status?.errors; 42 | if (errors !== undefined && errors.length > 0) { 43 | console.error('Container status errors: ', errors) 44 | } 45 | // Step 0. Get the details from the manifest 46 | const jsonFile: any = await jsonFileReader(this.manifestJsonFile); 47 | const expectedData = jsonFile['data']; 48 | if (expectedData['args']['request_dmitem'] !== this.requestDmitem) { 49 | throw new Error('Mismatch item id') 50 | } 51 | const fileBuf = Buffer.from(expectedData[expectedData['args']['main']]['$b'], 'hex') 52 | const main = expectedData['args']['main'] 53 | const mainHash = hash256(fileBuf).toString('hex') 54 | const proof = expectedData['args']['proof'] 55 | // Step 1. Query the container item to see if it's taken 56 | let bitworkc = 'any'; 57 | let bitworkr = 'any'; 58 | if (expectedData['args']['bitworkc']) { 59 | bitworkc = expectedData['args']['bitworkc']; 60 | } 61 | if (expectedData['args']['bitworkr']) { 62 | bitworkr = expectedData['args']['bitworkr']; 63 | } 64 | const getItemCmd = new GetContainerItemValidatedCommand(this.electrumApi, this.container, this.requestDmitem, bitworkc, bitworkr, main, mainHash, proof, this.checkWithoutSealed); 65 | const getItemCmdResponse = await getItemCmd.run(); 66 | return { 67 | success: true, 68 | data: getItemCmdResponse 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /lib/commands/get-container-item-validated-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | 4 | export class GetContainerItemValidatedCommand implements CommandInterface { 5 | constructor( 6 | private electrumApi: ElectrumApiInterface, 7 | private containerName: any, 8 | private item: any, 9 | private bitworkc: any, 10 | private bitworkr: any, 11 | private main: string, 12 | private mainHash: string, 13 | private proof: string, 14 | private checkWithoutSealed: boolean 15 | ) { 16 | } 17 | 18 | async run(): Promise { 19 | const responseResult = await this.electrumApi.atomicalsGetByContainerItemValidated(this.containerName, this.item, this.bitworkc, this.bitworkr, this.main, this.mainHash, this.proof, this.checkWithoutSealed); 20 | return { 21 | success: true, 22 | data: responseResult.result 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lib/commands/get-container-item.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | 4 | export class GetContainerItemCommand implements CommandInterface { 5 | constructor( 6 | private electrumApi: ElectrumApiInterface, 7 | private containerName: any, 8 | private item: any 9 | ) { 10 | } 11 | 12 | async run(): Promise { 13 | const responseResult = await this.electrumApi.atomicalsGetByContainerItem(this.containerName, this.item); 14 | return { 15 | success: true, 16 | data: responseResult.result 17 | } 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /lib/commands/get-container-items-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import { decorateAtomical } from "../utils/atomical-format-helpers"; 4 | import { GetCommand } from "./get-command"; 5 | 6 | export class GetContainerItems implements CommandInterface { 7 | constructor( 8 | private electrumApi: ElectrumApiInterface, 9 | private container: string, 10 | private limit: number, 11 | private offset: number 12 | ) { 13 | } 14 | async run(): Promise { 15 | const trimmedContainer = this.container.startsWith('#') ? this.container.substring(1) : this.container; 16 | const responseResult = await this.electrumApi.atomicalsGetContainerItems(trimmedContainer, this.limit, this.offset); 17 | if (!responseResult.result) { 18 | return { 19 | success: false, 20 | data: responseResult.result 21 | } 22 | } 23 | return { 24 | success: true, 25 | data: responseResult 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /lib/commands/get-dft-info-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import { decorateAtomical } from "../utils/atomical-format-helpers"; 4 | import { ResolveCommand } from "./resolve-command"; 5 | 6 | export class GetFtInfoCommand implements CommandInterface { 7 | constructor(private electrumApi: ElectrumApiInterface, 8 | private atomicalAliasOrId: string 9 | ) { 10 | } 11 | 12 | async run(): Promise { 13 | const command: CommandInterface = new ResolveCommand(this.electrumApi, this.atomicalAliasOrId, AtomicalsGetFetchType.GET); 14 | const resolved: any = await command.run(); 15 | let response; 16 | response = await this.electrumApi.atomicalsGetFtInfo(resolved.data.result.atomical_id); 17 | const updatedRes = Object.assign({}, 18 | response, 19 | { 20 | result: decorateAtomical(response.result) 21 | } 22 | ); 23 | return { 24 | success: true, 25 | data: updatedRes 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /lib/commands/get-global-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { decorateAtomical } from "../utils/atomical-format-helpers"; 3 | import { CommandInterface } from "./command.interface"; 4 | 5 | export class GetGlobalCommand implements CommandInterface { 6 | constructor( private electrumApi: ElectrumApiInterface, private hashes: number) { 7 | } 8 | 9 | async run(): Promise { 10 | let response = await this.electrumApi.atomicalsGetGlobal(this.hashes); 11 | const updatedRes = Object.assign({}, 12 | response, 13 | { 14 | result: decorateAtomical(response.result) 15 | } 16 | ); 17 | return { 18 | success: true, 19 | data: updatedRes 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /lib/commands/get-subrealm-info-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | 4 | export interface GetSubrealmInfoCommandResultInterface { 5 | success: boolean; 6 | data?: { 7 | 8 | }; 9 | message?: any; 10 | error?: any; 11 | } 12 | 13 | export class GetRealmInfoCommand implements CommandInterface { 14 | constructor( 15 | private electrumApi: ElectrumApiInterface, 16 | private realmOrSubrealm: any, 17 | ) { 18 | } 19 | 20 | async run(): Promise { 21 | const responseResult = await this.electrumApi.atomicalsGetRealmInfo(this.realmOrSubrealm); 22 | return { 23 | success: true, 24 | data: responseResult.result 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/commands/get-utxos.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | 5 | export class GetUtxosCommand implements CommandInterface { 6 | constructor( 7 | private electrumApi: ElectrumApiInterface, 8 | private address: string, 9 | ) { 10 | } 11 | 12 | async run(): Promise { 13 | const { scripthash } = detectAddressTypeToScripthash(this.address); 14 | return this.electrumApi.getUnspentScripthash(scripthash); 15 | } 16 | } -------------------------------------------------------------------------------- /lib/commands/list-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { decorateAtomicals } from "../utils/atomical-format-helpers"; 4 | export class ListCommand implements CommandInterface { 5 | constructor( 6 | private electrumApi: ElectrumApiInterface, 7 | private limit: number, 8 | private offset: number, 9 | private asc: boolean 10 | ) { 11 | } 12 | async run(): Promise { 13 | const response = await this.electrumApi.atomicalsList(this.limit, this.offset, this.asc); 14 | return Object.assign({}, 15 | response, 16 | { 17 | success: true, 18 | data: { 19 | global: response.global, 20 | result: decorateAtomicals(response.result), 21 | } 22 | } 23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /lib/commands/mint-interactive-container-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { ECPairFactory, ECPairAPI, TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { AtomicalOperationBuilder, ParentInputAtomical } from "../utils/atomical-operation-builder"; 11 | import { BaseRequestOptions } from "../interfaces/api.interface"; 12 | import { GetByContainerCommand } from "./get-by-container-command"; 13 | import { checkBaseRequestOptions, isValidBitworkMinimum, isValidBitworkString } from "../utils/atomical-format-helpers"; 14 | import { getAndCheckAtomicalInfo } from "./command-helpers"; 15 | import { getKeypairInfo } from "../utils/address-keypair-path"; 16 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 17 | initEccLib(tinysecp as any); 18 | const ECPair: ECPairAPI = ECPairFactory(tinysecp); 19 | export class MintInteractiveContainerCommand implements CommandInterface { 20 | constructor( 21 | private electrumApi: ElectrumApiInterface, 22 | private options: BaseRequestOptions, 23 | private requestContainer: string, 24 | private address: string, 25 | private fundingWIF: string, 26 | 27 | ) { 28 | this.options = checkBaseRequestOptions(this.options) 29 | this.requestContainer = this.requestContainer.startsWith('#') ? this.requestContainer.substring(1) : this.requestContainer; 30 | isValidBitworkMinimum(this.options.bitworkc); 31 | } 32 | async run(): Promise { 33 | // Check if the request already exists 34 | const getExistingNameCommand = new GetByContainerCommand(this.electrumApi, this.requestContainer, AtomicalsGetFetchType.GET); 35 | try { 36 | const getExistingNameResult = await getExistingNameCommand.run(); 37 | if (getExistingNameResult.success && getExistingNameResult.data) { 38 | if (getExistingNameResult.data.result && getExistingNameResult.data.result.atomical_id || getExistingNameResult.data.candidates.length) { 39 | throw 'Already exists with that name. Try a different name.'; 40 | } 41 | } 42 | } catch (err: any) { 43 | if (err.code !== 1) { 44 | throw err; // Code 1 means call correctly returned that it was not found 45 | } 46 | } 47 | 48 | const atomicalBuilder = new AtomicalOperationBuilder({ 49 | electrumApi: this.electrumApi, 50 | rbf: this.options.rbf, 51 | satsbyte: this.options.satsbyte, 52 | address: this.address, 53 | disableMiningChalk: this.options.disableMiningChalk, 54 | opType: 'nft', 55 | nftOptions: { 56 | satsoutput: this.options.satsoutput as any 57 | }, 58 | meta: this.options.meta, 59 | ctx: this.options.ctx, 60 | init: this.options.init, 61 | }); 62 | // Set to request a container 63 | atomicalBuilder.setRequestContainer(this.requestContainer); 64 | // Attach a container request 65 | if (this.options.container) 66 | atomicalBuilder.setContainerMembership(this.options.container); 67 | // Attach any requested bitwork 68 | if (this.options.bitworkc) { 69 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 70 | } 71 | if (this.options.bitworkr) { 72 | atomicalBuilder.setBitworkReveal(this.options.bitworkr); 73 | } 74 | 75 | if (this.options.parent) { 76 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.options.parent, this.options.parentOwner?.address as any); 77 | const parentKeypairInput = ECPair.fromWIF(this.options.parentOwner?.WIF as any); 78 | const parentKeypairInputInfo = getKeypairInfo(parentKeypairInput) 79 | const inp: ParentInputAtomical = { 80 | parentId: this.options.parent, 81 | parentUtxoPartial: inputUtxoPartial, 82 | parentKeyInfo: parentKeypairInputInfo 83 | } 84 | atomicalBuilder.setInputParent(inp) 85 | } 86 | 87 | if (this.options.parent) { 88 | atomicalBuilder.setInputParent(await AtomicalOperationBuilder.resolveInputParent(this.electrumApi, this.options.parent, this.options.parentOwner as any)) 89 | } 90 | 91 | // The receiver output of the mint 92 | atomicalBuilder.addOutput({ 93 | address: this.address, 94 | value: this.options.satsoutput as any 95 | }) 96 | 97 | const result = await atomicalBuilder.start(this.fundingWIF); 98 | return { 99 | success: true, 100 | data: result 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/commands/mint-interactive-dat-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { ECPairFactory, ECPairAPI, TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { prepareFilesDataAsObject, readFileAsCompleteDataObject } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { checkBaseRequestOptions } from "../utils/atomical-format-helpers"; 14 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 15 | initEccLib(tinysecp as any); 16 | const ECPair: ECPairAPI = ECPairFactory(tinysecp); 17 | export class MintInteractiveDatCommand implements CommandInterface { 18 | constructor( 19 | private electrumApi: ElectrumApiInterface, 20 | private options: BaseRequestOptions, 21 | private filepath: string, 22 | private givenFileName: string, 23 | private address: string, 24 | private fundingWIF: string, 25 | ) { 26 | this.options = checkBaseRequestOptions(this.options) 27 | } 28 | async run(): Promise { 29 | const atomicalBuilder = new AtomicalOperationBuilder({ 30 | electrumApi: this.electrumApi, 31 | rbf: this.options.rbf, 32 | satsbyte: this.options.satsbyte, 33 | address: this.address, 34 | disableMiningChalk: this.options.disableMiningChalk, 35 | opType: 'dat', 36 | datOptions: { 37 | satsoutput: 1000 38 | }, 39 | meta: this.options.meta, 40 | ctx: this.options.ctx, 41 | init: this.options.init, 42 | }); 43 | // Attach any default data 44 | let filesData = await readFileAsCompleteDataObject(this.filepath, this.givenFileName); 45 | await atomicalBuilder.setData(filesData); 46 | // Attach any requested bitwork 47 | if (this.options.bitworkc) { 48 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 49 | } 50 | if (this.options.bitworkr) { 51 | atomicalBuilder.setBitworkReveal(this.options.bitworkr); 52 | } 53 | 54 | if (this.options.parent) { 55 | atomicalBuilder.setInputParent(await AtomicalOperationBuilder.resolveInputParent(this.electrumApi, this.options.parent, this.options.parentOwner as any)) 56 | } 57 | 58 | // The receiver output of store data 59 | atomicalBuilder.addOutput({ 60 | address: this.address, 61 | value: this.options.satsoutput || 1000 62 | }); 63 | 64 | const result = await atomicalBuilder.start(this.fundingWIF); 65 | return { 66 | success: true, 67 | data: result 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /lib/commands/mint-interactive-ditem-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 11 | import { BaseRequestOptions } from "../interfaces/api.interface"; 12 | import { checkBaseRequestOptions } from "../utils/atomical-format-helpers"; 13 | import { GetByContainerCommand } from "./get-by-container-command"; 14 | import { jsonFileReader } from "../utils/file-utils"; 15 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 16 | import { GetContainerItemValidatedCommand } from "./get-container-item-validated-command"; 17 | import { hash256 } from "bitcoinjs-lib/src/crypto"; 18 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 19 | initEccLib(tinysecp as any); 20 | 21 | export class MintInteractiveDitemCommand implements CommandInterface { 22 | constructor( 23 | private electrumApi: ElectrumApiInterface, 24 | private options: BaseRequestOptions, 25 | private container: string, 26 | private requestDmitem: string, 27 | private manifestJsonFile: string, 28 | private address: string, 29 | private fundingWIF: string, 30 | ) { 31 | this.options = checkBaseRequestOptions(this.options) 32 | this.container = this.container.startsWith('#') ? this.container.substring(1) : this.container; 33 | } 34 | 35 | async run(): Promise { 36 | try { 37 | detectAddressTypeToScripthash(this.address); 38 | console.log("Initial mint address:", this.address); 39 | } catch (ex) { 40 | console.log('Error validating initial owner address'); 41 | throw ex; 42 | } 43 | const getCmd = new GetByContainerCommand(this.electrumApi, this.container, AtomicalsGetFetchType.GET); 44 | const getResponse = await getCmd.run(); 45 | if (!getResponse.success || !getResponse.data.result.atomical_id) { 46 | return { 47 | success: false, 48 | msg: 'Error retrieving container parent atomical ' + this.container, 49 | data: getResponse.data 50 | } 51 | } 52 | 53 | // Step 0. Get the details from the manifest 54 | const parentContainerId = getResponse.data.result.atomical_id; 55 | const jsonFile: any = await jsonFileReader(this.manifestJsonFile); 56 | const expectedData = jsonFile['data']; 57 | if (expectedData['args']['request_dmitem'] !== this.requestDmitem) { 58 | throw new Error('Mismatch item id') 59 | } 60 | const fileBuf = Buffer.from(expectedData[expectedData['args']['main']]['$b'], 'hex') 61 | const main = expectedData['args']['main'] 62 | const mainHash = hash256(fileBuf).toString('hex') 63 | const proof = expectedData['args']['proof'] 64 | 65 | // Step 1. Query the container item to see if it's taken 66 | const getItemCmd = new GetContainerItemValidatedCommand(this.electrumApi, this.container, this.requestDmitem, 'any', 'any', main, mainHash, proof, false); 67 | const getItemCmdResponse = await getItemCmd.run(); 68 | const data = getItemCmdResponse.data; 69 | console.log(getItemCmdResponse) 70 | if (data.atomical_id) { 71 | throw new Error('Container item is already claimed. Choose another item') 72 | } 73 | if (!data.proof_valid) { 74 | throw new Error('Item proof is invalid') 75 | } 76 | if (data.status) { 77 | throw new Error(`Item already contains status: ${data.status}`) 78 | } 79 | if (!data.applicable_rule) { 80 | throw new Error('No applicable rule') 81 | } 82 | if (data.applicable_rule.bitworkc || expectedData['args']['bitworkc']) { 83 | if (data.applicable_rule.bitworkc && expectedData['args']['bitworkc'] && (data.applicable_rule.bitworkc !== expectedData['args']['bitworkc'] && data.applicable_rule.bitworkc !== 'any')) { 84 | throw new Error('applicable_rule bitworkc is not compatible with the item args bitworkc') 85 | } 86 | } 87 | if (data.applicable_rule.bitworkr || expectedData['args']['bitworkr']) { 88 | if (data.applicable_rule.bitworkr && expectedData['args']['bitworkr'] && (data.applicable_rule.bitworkr !== expectedData['args']['bitworkr'] && data.applicable_rule.bitworkr !== 'any')) { 89 | throw new Error('applicable_rule bitworkr is not compatible with the item args bitworkr') 90 | } 91 | } 92 | 93 | const atomicalBuilder = new AtomicalOperationBuilder({ 94 | electrumApi: this.electrumApi, 95 | rbf: this.options.rbf, 96 | satsbyte: this.options.satsbyte, 97 | address: this.address, 98 | disableMiningChalk: this.options.disableMiningChalk, 99 | opType: 'nft', 100 | nftOptions: { 101 | satsoutput: this.options.satsoutput as any 102 | }, 103 | meta: this.options.meta, 104 | ctx: this.options.ctx, 105 | init: this.options.init, 106 | verbose: true 107 | }); 108 | 109 | // Set to request a container 110 | atomicalBuilder.setRequestItem(this.requestDmitem, parentContainerId); 111 | 112 | atomicalBuilder.setData({ 113 | [expectedData['args']['main']]: fileBuf 114 | }); 115 | 116 | // Attach any requested bitwork 117 | atomicalBuilder.setBitworkCommit(data.applicable_rule.bitworkc || expectedData['args']['bitworkc']); 118 | atomicalBuilder.setBitworkReveal(data.applicable_rule.bitworkr || expectedData['args']['bitworkr']); 119 | 120 | atomicalBuilder.setArgs({ 121 | ...expectedData['args'] 122 | }); 123 | 124 | // The receiver output 125 | atomicalBuilder.addOutput({ 126 | address: this.address, 127 | value: this.options.satsoutput as any || 1000 128 | }); 129 | const result = await atomicalBuilder.start(this.fundingWIF); 130 | 131 | return { 132 | success: true, 133 | data: result, 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/commands/mint-interactive-ft-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | 3 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 4 | import * as ecc from 'tiny-secp256k1'; 5 | import { hydrateConfig } from "../utils/hydrate-config"; 6 | import { TinySecp256k1Interface } from 'ecpair'; 7 | import * as readline from 'readline'; 8 | const bitcoin = require('bitcoinjs-lib'); 9 | bitcoin.initEccLib(ecc); 10 | import { 11 | initEccLib, 12 | } from "bitcoinjs-lib"; 13 | import { GetByTickerCommand } from "./get-by-ticker-command"; 14 | import { BaseRequestOptions } from "../interfaces/api.interface"; 15 | import { checkBaseRequestOptions, isValidBitworkMinimum, isValidBitworkString, isValidTickerName } from "../utils/atomical-format-helpers"; 16 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 17 | import { prepareFilesDataAsObject, readJsonFileAsCompleteDataObjectEncodeAtomicalIds } from "./command-helpers"; 18 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 19 | initEccLib(tinysecp as any); 20 | 21 | const promptContinue = async (): Promise => { 22 | const rl = readline.createInterface({ 23 | input: process.stdin, 24 | output: process.stdout 25 | }); 26 | 27 | try { 28 | let reply: string = ''; 29 | const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); 30 | while (reply !== 'q') { 31 | console.log(`Are you sure you want to continue with the details above? (y/n)`) 32 | console.log('-') 33 | reply = (await prompt("Enter your selection: ") as any); 34 | switch (reply) { 35 | case 'y': 36 | return true; 37 | default: 38 | throw new Error("user aborted") 39 | } 40 | } 41 | } finally { 42 | rl.close(); 43 | } 44 | } 45 | 46 | export class MintInteractiveFtCommand implements CommandInterface { 47 | constructor( 48 | private electrumApi: ElectrumApiInterface, 49 | private options: BaseRequestOptions, 50 | private file: string, 51 | private supply: number, 52 | private address: string, 53 | private requestTicker: string, 54 | private fundingWIF: string, 55 | ) { 56 | this.options = checkBaseRequestOptions(this.options) 57 | this.requestTicker = this.requestTicker.startsWith('$') ? this.requestTicker.substring(1) : this.requestTicker; 58 | isValidTickerName(requestTicker); 59 | isValidBitworkMinimum(this.options.bitworkc); 60 | } 61 | async run(): Promise { 62 | 63 | let filesData = await readJsonFileAsCompleteDataObjectEncodeAtomicalIds(this.file, true); 64 | console.log('Initializing Direct FT Token') 65 | console.log('-----------------------') 66 | console.log('Total Supply (Satoshis): ', this.supply); 67 | console.log('Total Supply (BTC): ', this.supply / 100000000); 68 | let supply = this.supply; 69 | 70 | let expandedSupply = supply; 71 | 72 | console.log('Total Supply: ', expandedSupply); 73 | console.log('Data objects: ', filesData); 74 | console.log('-----------------------') 75 | 76 | await promptContinue(); 77 | 78 | const getExistingNameCommand = new GetByTickerCommand(this.electrumApi, this.requestTicker, AtomicalsGetFetchType.GET); 79 | try { 80 | const getExistingNameResult = await getExistingNameCommand.run(); 81 | if (getExistingNameResult.success && getExistingNameResult.data) { 82 | if (getExistingNameResult.data.result && getExistingNameResult.data.result.atomical_id || getExistingNameResult.data.candidates.length) { 83 | throw 'Already exists with that name. Try a different name.'; 84 | } 85 | } 86 | } catch (err: any) { 87 | console.log('err', err) 88 | if (err.code !== 1) { 89 | throw err; // Code 1 means call correctly returned that it was not found 90 | } 91 | } 92 | 93 | const atomicalBuilder = new AtomicalOperationBuilder({ 94 | electrumApi: this.electrumApi, 95 | rbf: this.options.rbf, 96 | satsbyte: this.options.satsbyte, 97 | address: this.address, 98 | disableMiningChalk: this.options.disableMiningChalk, 99 | opType: 'ft', 100 | ftOptions: { 101 | fixedSupply: this.supply, 102 | ticker: this.requestTicker, 103 | }, 104 | meta: this.options.meta, 105 | ctx: this.options.ctx, 106 | init: this.options.init, 107 | }); 108 | 109 | // Attach any default data 110 | await atomicalBuilder.setData(filesData); 111 | // Set to request a container 112 | atomicalBuilder.setRequestTicker(this.requestTicker); 113 | // Attach a container request 114 | if (this.options.container) 115 | atomicalBuilder.setContainerMembership(this.options.container); 116 | // Attach any requested bitwork 117 | if (this.options.bitworkc) { 118 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 119 | } 120 | if (this.options.bitworkr) { 121 | atomicalBuilder.setBitworkReveal(this.options.bitworkr); 122 | } 123 | 124 | if (this.options.parent) { 125 | atomicalBuilder.setInputParent(await AtomicalOperationBuilder.resolveInputParent(this.electrumApi, this.options.parent, this.options.parentOwner as any)) 126 | } 127 | 128 | // The receiver output 129 | atomicalBuilder.addOutput({ 130 | address: this.address, 131 | value: this.supply 132 | }); 133 | 134 | const result = await atomicalBuilder.start(this.fundingWIF); 135 | return { 136 | success: true, 137 | data: result 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /lib/commands/mint-interactive-nft-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { prepareFilesDataAsObject, readJsonFileAsCompleteDataObjectEncodeAtomicalIds } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { checkBaseRequestOptions, isValidBitworkString } from "../utils/atomical-format-helpers"; 14 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 15 | initEccLib(tinysecp as any); 16 | export class MintInteractiveNftCommand implements CommandInterface { 17 | 18 | constructor( 19 | private electrumApi: ElectrumApiInterface, 20 | private options: BaseRequestOptions, 21 | private filename: string, 22 | private address: string, 23 | private fundingWIF: string, 24 | private jsonOnly?: boolean 25 | ) { 26 | this.options = checkBaseRequestOptions(this.options); 27 | } 28 | 29 | async run(): Promise { 30 | const atomicalBuilder = new AtomicalOperationBuilder({ 31 | electrumApi: this.electrumApi, 32 | rbf: this.options.rbf, 33 | satsbyte: this.options.satsbyte, 34 | address: this.address, 35 | disableMiningChalk: this.options.disableMiningChalk, 36 | opType: 'nft', 37 | nftOptions: { 38 | satsoutput: this.options.satsoutput as any 39 | }, 40 | meta: this.options.meta, 41 | ctx: this.options.ctx, 42 | init: this.options.init, 43 | }); 44 | // Attach any default data 45 | let filesData: any = null; 46 | if (this.jsonOnly) { 47 | filesData = await readJsonFileAsCompleteDataObjectEncodeAtomicalIds(this.filename); 48 | } else { 49 | filesData = await prepareFilesDataAsObject([this.filename]); 50 | } 51 | await atomicalBuilder.setData(filesData); 52 | // Attach a container request 53 | if (this.options.container) 54 | atomicalBuilder.setContainerMembership(this.options.container); 55 | // Attach any requested bitwork 56 | if (this.options.bitworkc) { 57 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 58 | } 59 | if (this.options.bitworkr) { 60 | atomicalBuilder.setBitworkReveal(this.options.bitworkr); 61 | } 62 | if (this.options.parent) { 63 | atomicalBuilder.setInputParent(await AtomicalOperationBuilder.resolveInputParent(this.electrumApi, this.options.parent, this.options.parentOwner as any)) 64 | } 65 | 66 | // The receiver output 67 | atomicalBuilder.addOutput({ 68 | address: this.address, 69 | value: this.options.satsoutput as any || 1000 70 | }); 71 | const result = await atomicalBuilder.start(this.fundingWIF); 72 | return { 73 | success: true, 74 | data: result 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /lib/commands/mint-interactive-realm-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | 3 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 4 | import * as ecc from 'tiny-secp256k1'; 5 | import { hydrateConfig } from "../utils/hydrate-config"; 6 | import { ECPairFactory, ECPairAPI, TinySecp256k1Interface } from 'ecpair'; 7 | const bitcoin = require('bitcoinjs-lib'); 8 | bitcoin.initEccLib(ecc); 9 | import { 10 | initEccLib, 11 | } from "bitcoinjs-lib"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { GetByRealmCommand } from "./get-by-realm-command"; 14 | import { checkBaseRequestOptions, isValidBitworkMinimum, isValidBitworkString } from "../utils/atomical-format-helpers"; 15 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 16 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 17 | initEccLib(tinysecp as any); 18 | const ECPair: ECPairAPI = ECPairFactory(tinysecp); 19 | export class MintInteractiveRealmCommand implements CommandInterface { 20 | constructor( 21 | private electrumApi: ElectrumApiInterface, 22 | private options: BaseRequestOptions, 23 | private requestRealm: string, 24 | private address: string, 25 | private fundingWIF: string, 26 | ) { 27 | this.options = checkBaseRequestOptions(this.options); 28 | this.requestRealm = this.requestRealm.startsWith('+') ? this.requestRealm.substring(1) : this.requestRealm; 29 | isValidBitworkMinimum(this.options.bitworkc); 30 | } 31 | async run(): Promise { 32 | // Check if the request already exists 33 | const getExistingNameCommand = new GetByRealmCommand(this.electrumApi, this.requestRealm, AtomicalsGetFetchType.GET); 34 | try { 35 | const getExistingNameResult = await getExistingNameCommand.run(); 36 | if (getExistingNameResult.success && getExistingNameResult.data) { 37 | if (getExistingNameResult.data.result && getExistingNameResult.data.result.atomical_id || getExistingNameResult.data.candidates.length) { 38 | throw 'Already exists with that name. Try a different name.'; 39 | } 40 | } 41 | } catch (err: any) { 42 | if (err.code !== 1) { 43 | throw err; // Code 1 means call correctly returned that it was not found 44 | } 45 | } 46 | 47 | const atomicalBuilder = new AtomicalOperationBuilder({ 48 | electrumApi: this.electrumApi, 49 | rbf: this.options.rbf, 50 | satsbyte: this.options.satsbyte, 51 | address: this.address, 52 | disableMiningChalk: this.options.disableMiningChalk, 53 | opType: 'nft', 54 | nftOptions: { 55 | satsoutput: this.options.satsoutput as any 56 | }, 57 | meta: this.options.meta, 58 | ctx: this.options.ctx, 59 | init: this.options.init, 60 | }); 61 | // Set to request a container 62 | atomicalBuilder.setRequestRealm(this.requestRealm); 63 | // Attach a container request 64 | if (this.options.container) 65 | atomicalBuilder.setContainerMembership(this.options.container); 66 | // Attach any requested bitwork 67 | if (this.options.bitworkc) { 68 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 69 | } 70 | if (this.options.bitworkr) { 71 | atomicalBuilder.setBitworkReveal(this.options.bitworkr); 72 | } 73 | 74 | if (this.options.parent) { 75 | atomicalBuilder.setInputParent(await AtomicalOperationBuilder.resolveInputParent(this.electrumApi, this.options.parent, this.options.parentOwner as any)) 76 | } 77 | 78 | // The receiver output 79 | atomicalBuilder.addOutput({ 80 | address: this.address, 81 | value: this.options.satsoutput as any || 1000 82 | }); 83 | const result = await atomicalBuilder.start(this.fundingWIF); 84 | return { 85 | success: true, 86 | data: result 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /lib/commands/mint-interactive-subrealm-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { ECPairFactory, ECPairAPI, TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { IsAtomicalOwnedByWalletRecord, detectAddressTypeToScripthash } from "../utils/address-helpers"; 11 | import { logBanner } from "./command-helpers"; 12 | import { AtomicalStatus } from "../interfaces/atomical-status.interface"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | import { GetCommand } from "./get-command"; 15 | import { MintInteractiveSubrealmDirectCommand } from "./mint-interactive-subrealm-direct-command"; 16 | import { GetRealmInfoCommand } from "./get-subrealm-info-command"; 17 | import { BaseRequestOptions } from "../interfaces/api.interface"; 18 | import { MintInteractiveSubrealmWithRulesCommand } from "./mint-interactive-subrealm-with-rules-command"; 19 | import { checkBaseRequestOptions } from "../utils/atomical-format-helpers"; 20 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 21 | initEccLib(tinysecp as any); 22 | const ECPair: ECPairAPI = ECPairFactory(tinysecp); 23 | 24 | export interface ResolvedRealm { 25 | atomical: AtomicalStatus 26 | } 27 | 28 | export class MintInteractiveSubrealmCommand implements CommandInterface { 29 | constructor( 30 | private electrumApi: ElectrumApiInterface, 31 | private options: BaseRequestOptions, 32 | private requestSubRealm: string, 33 | private address: string, 34 | private fundingWIF: string, 35 | private owner: IWalletRecord, 36 | ) { 37 | this.options = checkBaseRequestOptions(this.options) 38 | this.requestSubRealm = this.requestSubRealm.startsWith('+') ? this.requestSubRealm.substring(1) : this.requestSubRealm; 39 | } 40 | 41 | async run(): Promise { 42 | 43 | if (this.requestSubRealm.indexOf('.') === -1) { 44 | throw 'Cannot mint for a top level Realm. Must be a name like +name.subname. Use the mint-realm command for a top level Realm'; 45 | } 46 | 47 | const realmParts = this.requestSubRealm.split('.'); 48 | const finalSubrealmPart = realmParts[realmParts.length - 1]; 49 | // Validate that the addresses are valid 50 | try { 51 | detectAddressTypeToScripthash(this.address); 52 | console.log("Initial mint address:", this.address); 53 | } catch (ex) { 54 | console.log('Error validating initial owner address'); 55 | throw ex; 56 | } 57 | // Step 1. Query the full realm and determine if it's already claimed 58 | const getSubrealmCommand = new GetRealmInfoCommand(this.electrumApi, this.requestSubRealm); 59 | const getSubrealmReponse = await getSubrealmCommand.run(); 60 | console.log('getSubrealmReponse', JSON.stringify(getSubrealmReponse.data, null, 2)); 61 | 62 | if (getSubrealmReponse.data.atomical_id) { 63 | return { 64 | success: false, 65 | msg: 'Subrealm is already claimed. Choose another Subrealm', 66 | data: getSubrealmReponse.data 67 | } 68 | } 69 | // Step 2. Check to make sure the only missing part is the requested subrealm itself 70 | if (getSubrealmReponse.data.missing_name_parts !== finalSubrealmPart) { 71 | return { 72 | success: false, 73 | msg: 'Subrealm cannot be minted because at least one other realm parent is missing. Mint that realm first if possible.', 74 | data: getSubrealmReponse.data 75 | } 76 | } 77 | // Step 3. Check if the nearest parent is actually a parent realm that the current client already owns 78 | // by fetching and comparing the address at the location. 79 | const nearestParentAtomicalId = getSubrealmReponse.data.nearest_parent_realm_atomical_id; 80 | const getNearestParentRealmCommand = new GetCommand(this.electrumApi, nearestParentAtomicalId, AtomicalsGetFetchType.LOCATION); 81 | const getNearestParentRealmResponse = await getNearestParentRealmCommand.run(); 82 | 83 | const hasValidParent = getNearestParentRealmResponse.success && getNearestParentRealmResponse.data?.result?.atomical_id == nearestParentAtomicalId; 84 | if (!hasValidParent) { 85 | return { 86 | success: false, 87 | msg: 'Error retrieving nearest parent atomical ' + nearestParentAtomicalId, 88 | data: getNearestParentRealmResponse.data 89 | } 90 | } 91 | 92 | // If it's owned by self, then we can mint directly provided that there is no other candidates 93 | if (IsAtomicalOwnedByWalletRecord(this.owner.address, getNearestParentRealmResponse.data.result)) { 94 | logBanner('DETECTED PARENT REALM IS OWNED BY SELF'); 95 | const commandMintDirect = new MintInteractiveSubrealmDirectCommand( 96 | this.electrumApi, 97 | this.requestSubRealm, 98 | nearestParentAtomicalId, 99 | this.address, 100 | this.fundingWIF, 101 | this.owner, 102 | this.options); 103 | const commandMintDirectResponse = await commandMintDirect.run(); 104 | if (commandMintDirectResponse.success) { 105 | return { 106 | success: true, 107 | data: commandMintDirectResponse.data 108 | } 109 | } else { 110 | return { 111 | success: false, 112 | data: commandMintDirectResponse.data 113 | } 114 | } 115 | } else { 116 | logBanner('DETECTED PARENT REALM IS NOT OWNED BY PROVIDED --OWNER WALLET'); 117 | console.log('Proceeding to mint with the available subrealm minting rules (if available)...') 118 | const commandMintWithRules = new MintInteractiveSubrealmWithRulesCommand( 119 | this.electrumApi, 120 | this.requestSubRealm, 121 | nearestParentAtomicalId, 122 | this.address, 123 | this.fundingWIF, 124 | this.options); 125 | const commandMintWithRulesResponse = await commandMintWithRules.run(); 126 | if (commandMintWithRulesResponse.success) { 127 | return { 128 | success: true, 129 | data: commandMintWithRulesResponse.data 130 | } 131 | } return { 132 | success: false, 133 | data: commandMintWithRulesResponse.data 134 | } 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /lib/commands/mint-interactive-subrealm-direct-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { ECPairFactory, ECPairAPI, TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { IsAtomicalOwnedByWalletRecord } from "../utils/address-helpers"; 11 | import { AtomicalStatus } from "../interfaces/atomical-status.interface"; 12 | import { GetRealmInfoCommand } from "./get-subrealm-info-command"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | import { GetCommand } from "./get-command"; 15 | import { warnContinueAbort } from "../utils/prompt-helpers"; 16 | import { BaseRequestOptions } from "../interfaces/api.interface"; 17 | import { checkBaseRequestOptions } from "../utils/atomical-format-helpers"; 18 | import { AtomicalOperationBuilder, REALM_CLAIM_TYPE } from "../utils/atomical-operation-builder"; 19 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 20 | initEccLib(tinysecp as any); 21 | const ECPair: ECPairAPI = ECPairFactory(tinysecp); 22 | 23 | export interface ResolvedRealm { 24 | atomical: AtomicalStatus 25 | } 26 | 27 | /** 28 | * Mints a subrealm with the assumption that the `owner` wallet owns the parent atomical 29 | */ 30 | export class MintInteractiveSubrealmDirectCommand implements CommandInterface { 31 | constructor( 32 | private electrumApi: ElectrumApiInterface, 33 | private requestSubrealm: string, 34 | private nearestParentAtomicalId: string, 35 | private address: string, 36 | private fundingWIF: string, 37 | private owner: IWalletRecord, 38 | private options: BaseRequestOptions 39 | ) { 40 | this.options = checkBaseRequestOptions(this.options) 41 | } 42 | 43 | async run(): Promise { 44 | 45 | if (this.requestSubrealm.indexOf('.') === -1) { 46 | throw 'Cannot mint for a top level Realm. Must be a name like +name.subname. Use the mint-realm command for a top level Realm'; 47 | } 48 | 49 | // Step 1. Query the full realm and determine if it's already claimed 50 | const getSubrealmCommand = new GetRealmInfoCommand(this.electrumApi, this.requestSubrealm); 51 | const getSubrealmReponse = await getSubrealmCommand.run(); 52 | if (getSubrealmReponse.data.atomical_id) { 53 | return { 54 | success: false, 55 | msg: 'Subrealm is already claimed. Choose another Subrealm', 56 | data: getSubrealmReponse.data 57 | } 58 | } 59 | const finalSubrealmSplit = this.requestSubrealm.split('.'); 60 | const finalSubrealm = finalSubrealmSplit[finalSubrealmSplit.length - 1]; 61 | if (!getSubrealmReponse.data.nearest_parent_realm_atomical_id) { 62 | throw new Error('Nearest parent realm id is not set') 63 | } 64 | if (getSubrealmReponse.data.missing_name_parts !== finalSubrealm) { 65 | throw new Error(`Nearest parent realm is not the direct potential parent of the requested Subrealm. Try minting the parents first first`) 66 | } 67 | const candidates = getSubrealmReponse.data.candidates 68 | 69 | if (candidates.length) { 70 | await warnContinueAbort('Candidate Subrealm exists already. There is no guarantee you will win the subrealm. Continue anyways (y/n)?', 'y'); 71 | } 72 | const getNearestParentRealmCommand = new GetCommand( this.electrumApi, this.nearestParentAtomicalId, AtomicalsGetFetchType.LOCATION); 73 | const getNearestParentRealmResponse = await getNearestParentRealmCommand.run(); 74 | 75 | const hasValidParent = getNearestParentRealmResponse.success && getNearestParentRealmResponse.data?.result?.atomical_id == this.nearestParentAtomicalId; 76 | if (!hasValidParent) { 77 | return { 78 | success: false, 79 | msg: 'Error retrieving nearest parent atomical ' + this.nearestParentAtomicalId, 80 | data: getNearestParentRealmResponse.data 81 | } 82 | } 83 | // If it's owned by self, then we can mint directly provided that there is no other candidates 84 | const utxoLocation = IsAtomicalOwnedByWalletRecord(this.owner.address, getNearestParentRealmResponse.data.result); 85 | if (!utxoLocation) { 86 | throw new Error('Parent realm is not owned by self') 87 | } 88 | if (this.nearestParentAtomicalId !== getNearestParentRealmResponse.data.result.atomical_id) { 89 | throw new Error('Provided parent id does not match the current location of the parent realm') 90 | } 91 | 92 | const atomicalBuilder = new AtomicalOperationBuilder({ 93 | electrumApi: this.electrumApi, 94 | rbf: this.options.rbf, 95 | satsbyte: this.options.satsbyte, 96 | address: this.address, 97 | disableMiningChalk: this.options.disableMiningChalk, 98 | opType: 'nft', 99 | nftOptions: { 100 | satsoutput: this.options.satsoutput as any 101 | }, 102 | meta: this.options.meta, 103 | ctx: this.options.ctx, 104 | init: this.options.init, 105 | }); 106 | 107 | // For direct mints we must spent the parent realm atomical in the same transaction 108 | atomicalBuilder.addInputUtxo(utxoLocation, this.owner.WIF) 109 | 110 | // The first output will be the location of the subrealm minted 111 | atomicalBuilder.addOutput({ 112 | address: this.address, 113 | value: this.options.satsoutput as number, 114 | }); 115 | 116 | // ... and make sure to assign an output to capture the spent parent realm atomical 117 | atomicalBuilder.addOutput({ 118 | address: utxoLocation.address, 119 | value: utxoLocation.witnessUtxo.value 120 | }); 121 | 122 | // Set to request a container 123 | atomicalBuilder.setRequestSubrealm(this.requestSubrealm, this.nearestParentAtomicalId, REALM_CLAIM_TYPE.DIRECT); 124 | // Attach a container request 125 | if (this.options.container) 126 | atomicalBuilder.setContainerMembership(this.options.container); 127 | // Attach any requested bitwork 128 | if (this.options.bitworkc) { 129 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 130 | } 131 | if (this.options.bitworkr) { 132 | atomicalBuilder.setBitworkReveal(this.options.bitworkr); 133 | } 134 | // The receiver output 135 | atomicalBuilder.addOutput({ 136 | address: this.address, 137 | value: this.options.satsoutput as any || 1000 138 | }); 139 | const result = await atomicalBuilder.start(this.fundingWIF); 140 | return { 141 | success: true, 142 | data: result 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /lib/commands/render-previews-command.ts: -------------------------------------------------------------------------------- 1 | import { CommandInterface } from "./command.interface"; 2 | import { FileMap } from "../interfaces/filemap.interface"; 3 | 4 | export function isImage(contentType: string): boolean { 5 | return /^image\/(jpe?g|png|gif|bmp|webp|svg)$/.test(contentType); 6 | } 7 | 8 | export function isText(contentType: string): boolean { 9 | return /utf\-?8|application\/json|text\/plain|markdown|xml|html/.test(contentType) 10 | } 11 | 12 | export class RenderPreviewsCommand implements CommandInterface { 13 | constructor(private filesmap: FileMap, private body: boolean 14 | ) { 15 | } 16 | async run(): Promise { 17 | for (const inputIndex in this.filesmap) { 18 | if (!this.filesmap.hasOwnProperty(inputIndex)) { 19 | continue; 20 | } 21 | console.log(`-------------------------------------------------`); 22 | console.log(`Rendering files at inputIndex ${inputIndex}`); 23 | for (const filename in this.filesmap[inputIndex].files) { 24 | if (!this.filesmap[inputIndex].files.hasOwnProperty(filename)) { 25 | continue; 26 | } 27 | console.log(`-------------------------------------------------`); 28 | console.log(`Rendering file ${filename}`); 29 | const filepath = this.filesmap[inputIndex].files[filename].fullPath; 30 | const contentType = this.filesmap[inputIndex].files[filename].contentType; 31 | const contentLength = this.filesmap[inputIndex].files[filename].contentLength; 32 | const body = this.filesmap[inputIndex].files[filename].body; 33 | console.log('File name: ', filename); 34 | console.log('Full path: ', filepath); 35 | console.log('Content Type: ', contentType); 36 | console.log('Content Length: ', contentLength); 37 | if (this.body) { 38 | console.log('Body (hex encoded): ', this.filesmap[inputIndex].files[filename].body); 39 | } 40 | if (isImage(contentType)) { 41 | const {default: terminalImage} = await import("terminal-image"); 42 | console.log(await terminalImage.file(filepath)); 43 | } else if (isText(contentType)) { 44 | console.log('Body decoded: '); 45 | console.log(Buffer.from(this.filesmap[inputIndex].files[filename].body, 'hex').toString('utf8')); 46 | } else { 47 | console.log(`File is not an image or text-like. View file manually at ${filepath}`) 48 | } 49 | } 50 | } 51 | /* 52 | displayImage.fromFile("banner.png").then(image => { 53 | console.log(image) 54 | }) 55 | 56 | displayImage.fromFile("shadow.gif").then(image => { 57 | console.log(image) 58 | }) 59 | 60 | displayImage.fromFile("ape.png").then(image => { 61 | console.log(image) 62 | }) 63 | 64 | displayImage.fromFile("pepe.png").then(image => { 65 | console.log(image) 66 | }) */ 67 | 68 | return null; 69 | } 70 | } 71 | 72 | /* 73 | 74 | // Remove the body by default 75 | if (!body) { 76 | for (const inputIndex in result.data.filemap) { 77 | if (!result.data.filemap.hasOwnProperty(inputIndex)) { 78 | continue; 79 | } 80 | for (const filename in result.data.filemap[inputIndex].files) { 81 | if (!result.data.filemap[inputIndex].files.hasOwnProperty(filename)) { 82 | continue; 83 | } 84 | const fileEntry = result.data.filemap[inputIndex].files[filename]; 85 | delete fileEntry['body']; 86 | } 87 | } 88 | } 89 | 90 | */ -------------------------------------------------------------------------------- /lib/commands/resolve-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { AtomicalsGetFetchType, CommandInterface } from "./command.interface"; 3 | import { AtomicalIdentifierType, AtomicalResolvedIdentifierReturn, decorateAtomical, getAtomicalIdentifierType } from "../utils/atomical-format-helpers"; 4 | import { GetByRealmCommand } from "./get-by-realm-command"; 5 | import { GetByContainerCommand } from "./get-by-container-command"; 6 | import { GetByTickerCommand } from "./get-by-ticker-command"; 7 | import { GetCommand } from "./get-command"; 8 | 9 | export class ResolveCommand implements CommandInterface { 10 | constructor( 11 | private electrumApi: ElectrumApiInterface, 12 | private atomicalAliasOrId: any, 13 | private fetchType: AtomicalsGetFetchType = AtomicalsGetFetchType.GET, 14 | ) { 15 | } 16 | 17 | async run(): Promise { 18 | const atomicalType: AtomicalResolvedIdentifierReturn = getAtomicalIdentifierType(this.atomicalAliasOrId); 19 | let foundAtomicalResponse; 20 | let cmd; 21 | if (atomicalType.type === AtomicalIdentifierType.ATOMICAL_ID || atomicalType.type === AtomicalIdentifierType.ATOMICAL_NUMBER) { 22 | cmd = new GetCommand(this.electrumApi, atomicalType.providedIdentifier || '', this.fetchType); 23 | } else if (atomicalType.type === AtomicalIdentifierType.REALM_NAME) { 24 | cmd = new GetByRealmCommand(this.electrumApi, atomicalType.realmName || '', this.fetchType); 25 | } else if (atomicalType.type === AtomicalIdentifierType.CONTAINER_NAME) { 26 | cmd = new GetByContainerCommand(this.electrumApi, atomicalType.containerName || '', this.fetchType); 27 | } else if (atomicalType.type === AtomicalIdentifierType.TICKER_NAME) { 28 | cmd = new GetByTickerCommand(this.electrumApi, atomicalType.tickerName || '', this.fetchType); 29 | } 30 | const cmdResponse = await cmd.run(); 31 | if (!cmdResponse || !cmdResponse.success) { 32 | return cmdResponse; 33 | } 34 | foundAtomicalResponse = cmdResponse.data; 35 | const updatedRes = Object.assign({}, 36 | foundAtomicalResponse, 37 | { 38 | result: decorateAtomical(foundAtomicalResponse.result) 39 | } 40 | ); 41 | return { 42 | success: true, 43 | data: updatedRes 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/commands/seal-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { getAndCheckAtomicalInfo, logBanner } from "./command-helpers"; 11 | 12 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 13 | import { BaseRequestOptions } from "../interfaces/api.interface"; 14 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 15 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 16 | initEccLib(tinysecp as any); 17 | 18 | export class SealInteractiveCommand implements CommandInterface { 19 | constructor( 20 | private electrumApi: ElectrumApiInterface, 21 | private options: BaseRequestOptions, 22 | private atomicalId: string, 23 | private owner: IWalletRecord, 24 | private funding: IWalletRecord, 25 | ) { 26 | } 27 | 28 | async run(): Promise { 29 | logBanner(`Seal Interactive`); 30 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.atomicalId, this.owner.address); 31 | const atomicalBuilder = new AtomicalOperationBuilder({ 32 | electrumApi: this.electrumApi, 33 | rbf: this.options.rbf, 34 | satsbyte: this.options.satsbyte, 35 | address: this.owner.address, 36 | disableMiningChalk: this.options.disableMiningChalk, 37 | opType: 'sl', 38 | nftOptions: { 39 | satsoutput: this.options.satsoutput as any 40 | }, 41 | meta: this.options.meta, 42 | ctx: this.options.ctx, 43 | init: this.options.init, 44 | }); 45 | 46 | // Add the atomical to update 47 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 48 | 49 | if (this.options.bitworkc) { 50 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 51 | } 52 | 53 | // The receiver output 54 | atomicalBuilder.addOutput({ 55 | address: this.owner.address, 56 | value: this.options.satsoutput as any || 1000 57 | }); 58 | 59 | const result = await atomicalBuilder.start(this.funding.WIF); 60 | return { 61 | success: true, 62 | data: result 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /lib/commands/search-containers-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | export class SearchContainersCommand implements CommandInterface { 4 | constructor( 5 | private electrumApi: ElectrumApiInterface, 6 | private prefix: string, 7 | private asc?: boolean 8 | ) { 9 | } 10 | async run(): Promise { 11 | const responseResult = await this.electrumApi.atomicalsFindContainers(this.prefix, this.asc); 12 | if (!responseResult.result) { 13 | return { 14 | success: false, 15 | data: responseResult 16 | } 17 | } else if (responseResult.result) { 18 | return { 19 | success: true, 20 | data: responseResult.result 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /lib/commands/search-realms-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | export class SearchRealmsCommand implements CommandInterface { 4 | constructor( 5 | private electrumApi: ElectrumApiInterface, 6 | private prefix: string, 7 | private asc?: boolean, 8 | ) { 9 | } 10 | 11 | async run(): Promise { 12 | const responseResult = await this.electrumApi.atomicalsFindRealms(this.prefix, this.asc); 13 | if (!responseResult.result) { 14 | return { 15 | success: false, 16 | data: responseResult 17 | } 18 | } else if (responseResult.result) { 19 | return { 20 | success: true, 21 | data: responseResult.result 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lib/commands/search-tickers-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | export class SearchTickersCommand implements CommandInterface { 4 | constructor( 5 | private electrumApi: ElectrumApiInterface, 6 | private prefix: string | null, 7 | private asc?: boolean, 8 | ) { 9 | } 10 | async run(): Promise { 11 | const responseResult = await this.electrumApi.atomicalsFindTickers(this.prefix, this.asc); 12 | if (!responseResult.result) { 13 | return { 14 | success: false, 15 | data: responseResult 16 | } 17 | } else if (responseResult.result) { 18 | return { 19 | success: true, 20 | data: responseResult.result 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /lib/commands/server-version-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | export class ServerVersionCommand implements CommandInterface { 4 | constructor(private electrumApi: ElectrumApiInterface) {} 5 | async run(): Promise { 6 | const result = await this.electrumApi.serverVersion(); 7 | return result; 8 | } 9 | } -------------------------------------------------------------------------------- /lib/commands/set-container-data-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { getAndCheckAtomicalInfo, logBanner, prepareFilesDataAsObject, readJsonFileAsCompleteDataObjectEncodeAtomicalIds } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | 15 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 16 | initEccLib(tinysecp as any); 17 | 18 | export class SetContainerDataInteractiveCommand implements CommandInterface { 19 | constructor( 20 | private electrumApi: ElectrumApiInterface, 21 | private options: BaseRequestOptions, 22 | private containerName: string, 23 | private filename: string, 24 | private owner: IWalletRecord, 25 | private funding: IWalletRecord, 26 | ) { 27 | 28 | } 29 | async run(): Promise { 30 | logBanner(`Set Container Data Interactive`); 31 | // Attach any default data 32 | let filesData = await readJsonFileAsCompleteDataObjectEncodeAtomicalIds(this.filename, true); 33 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.containerName, this.owner.address, 'NFT', 'container'); 34 | const atomicalBuilder = new AtomicalOperationBuilder({ 35 | electrumApi: this.electrumApi, 36 | rbf: this.options.rbf, 37 | satsbyte: this.options.satsbyte, 38 | address: this.owner.address, 39 | disableMiningChalk: this.options.disableMiningChalk, 40 | opType: 'mod', 41 | nftOptions: { 42 | satsoutput: this.options.satsoutput as any 43 | }, 44 | meta: this.options.meta, 45 | ctx: this.options.ctx, 46 | init: this.options.init, 47 | }); 48 | await atomicalBuilder.setData(filesData); 49 | 50 | // Attach any requested bitwork 51 | if (this.options.bitworkc) { 52 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 53 | } 54 | // Add the atomical to update 55 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 56 | 57 | // The receiver output 58 | atomicalBuilder.addOutput({ 59 | address: this.owner.address, 60 | value: this.options.satsoutput as any || 1000// todo: determine how to auto detect the total input and set it to that 61 | }); 62 | 63 | const result = await atomicalBuilder.start(this.funding.WIF); 64 | return { 65 | success: true, 66 | data: result 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /lib/commands/set-container-dmint-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { getAndCheckAtomicalInfo, logBanner, readJsonFileAsCompleteDataObjectEncodeAtomicalIds } from "./command-helpers"; 11 | import { isAtomicalId, isValidBitworkString } from "../utils/atomical-format-helpers"; 12 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 13 | import { BaseRequestOptions } from "../interfaces/api.interface"; 14 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 15 | import { detectScriptToAddressType } from "../utils/address-helpers"; 16 | 17 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 18 | initEccLib(tinysecp as any); 19 | 20 | interface DmintManifestInteface { 21 | v: string, 22 | mint_height: number, 23 | items: number, 24 | rules: { 25 | o?: { [script: string]: {v: number, id?: string} }, 26 | p: string, 27 | bitworkc?: string, 28 | bitworkr?: string, 29 | }[], 30 | } 31 | 32 | export function validateDmint( 33 | obj: {dmint?: DmintManifestInteface} | undefined, 34 | ) { 35 | if (!obj) { 36 | throw `Invalid manifest.`; 37 | } 38 | const dmint = obj.dmint; 39 | if (!dmint) { 40 | throw `Invalid manifest: No 'dmint' field.`; 41 | } 42 | const items = dmint.items; 43 | if (!items) { 44 | throw `Invalid items count: ${items}.`; 45 | } 46 | for (const {o, p, bitworkc, bitworkr} of dmint.rules) { 47 | try { 48 | new RegExp(p); 49 | } catch (e) { 50 | throw `Invalid rule pattern: ${p}.\n${e}`; 51 | } 52 | if (o === undefined && bitworkc === undefined && bitworkr === undefined) { 53 | throw `Invalid rule (${p}): No fields specified.`; 54 | } 55 | if (o !== undefined) { 56 | if (Object.keys(o).length === 0) { 57 | throw `Invalid rule (${p}) output: No script specified.` 58 | } 59 | for (const entry of Object.entries(o)) { 60 | const script = entry[0] 61 | try { 62 | detectScriptToAddressType(script) 63 | } catch (e) { 64 | throw `Invalid rule (${p}) output script [${script}]: ${e}` 65 | } 66 | const {v, id} = entry[1] 67 | if (typeof v !== 'number' || !v || v <= 0) { 68 | throw `Invalid rule (${p}) output value: Invalid amount (${v}).` 69 | } 70 | if (id !== undefined && !isAtomicalId(id)) { 71 | throw `Invalid rule (${p}) output id: Invalid Atomical ID (${id}).` 72 | } 73 | } 74 | } 75 | if (bitworkc !== undefined && !isValidBitworkString(bitworkc)) { 76 | throw `Invalid rule (${p}) bitworkc: Invalid bitwork string (${bitworkc}).` 77 | } 78 | if (bitworkr !== undefined && !isValidBitworkString(bitworkr)) { 79 | throw `Invalid rule (${p}) bitworkr: Invalid bitwork string (${bitworkr}).` 80 | } 81 | } 82 | const mh = dmint.mint_height; 83 | if (mh === 0) { 84 | return true; 85 | } 86 | if (mh !== undefined) { 87 | if (isNaN(mh)) { 88 | throw `Invalid mint height: NaN.` 89 | } 90 | if (mh < 0 || mh > 10000000) { 91 | throw `Invalid mint height: Should between 0 and 10000000.` 92 | } 93 | return true; 94 | } 95 | return false; 96 | } 97 | 98 | export class SetContainerDmintInteractiveCommand implements CommandInterface { 99 | constructor( 100 | private electrumApi: ElectrumApiInterface, 101 | private options: BaseRequestOptions, 102 | private containerName: string, 103 | private filename: string, 104 | private owner: IWalletRecord, 105 | private funding: IWalletRecord, 106 | ) { 107 | 108 | } 109 | async run(): Promise { 110 | logBanner(`Set Container Data Interactive`); 111 | // Attach any default data 112 | let filesData = await readJsonFileAsCompleteDataObjectEncodeAtomicalIds(this.filename, false); 113 | 114 | if (!validateDmint(filesData)) { 115 | throw new Error('Invalid dmint'); 116 | } 117 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.containerName, this.owner.address, 'NFT', 'container'); 118 | const atomicalBuilder = new AtomicalOperationBuilder({ 119 | electrumApi: this.electrumApi, 120 | rbf: this.options.rbf, 121 | satsbyte: this.options.satsbyte, 122 | address: this.owner.address, 123 | disableMiningChalk: this.options.disableMiningChalk, 124 | opType: 'mod', 125 | nftOptions: { 126 | satsoutput: this.options.satsoutput as any 127 | }, 128 | meta: this.options.meta, 129 | ctx: this.options.ctx, 130 | init: this.options.init, 131 | }); 132 | await atomicalBuilder.setData(filesData); 133 | 134 | // Attach any requested bitwork 135 | if (this.options.bitworkc) { 136 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 137 | } 138 | // Add the atomical to update 139 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 140 | 141 | // The receiver output 142 | atomicalBuilder.addOutput({ 143 | address: this.owner.address, 144 | value: this.options.satsoutput as any || 1000// todo: determine how to auto detect the total input and set it to that 145 | }); 146 | 147 | const result = await atomicalBuilder.start(this.funding.WIF); 148 | return { 149 | success: true, 150 | data: result 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /lib/commands/set-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { getAndCheckAtomicalInfo, logBanner, prepareFilesDataAsObject, readJsonFileAsCompleteDataObjectEncodeAtomicalIds } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | 15 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 16 | initEccLib(tinysecp as any); 17 | 18 | export class SetInteractiveCommand implements CommandInterface { 19 | constructor( 20 | private electrumApi: ElectrumApiInterface, 21 | private options: BaseRequestOptions, 22 | private atomicalId: string, 23 | private filename: string, 24 | private owner: IWalletRecord, 25 | private funding: IWalletRecord, 26 | ) { 27 | 28 | } 29 | async run(): Promise { 30 | logBanner(`Set Interactive`); 31 | // Attach any default data 32 | let filesData = await readJsonFileAsCompleteDataObjectEncodeAtomicalIds(this.filename); 33 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.atomicalId, this.owner.address); 34 | const atomicalBuilder = new AtomicalOperationBuilder({ 35 | electrumApi: this.electrumApi, 36 | rbf: this.options.rbf, 37 | satsbyte: this.options.satsbyte, 38 | address: this.owner.address, 39 | disableMiningChalk: this.options.disableMiningChalk, 40 | opType: 'mod', 41 | nftOptions: { 42 | satsoutput: this.options.satsoutput as any 43 | }, 44 | meta: this.options.meta, 45 | ctx: this.options.ctx, 46 | init: this.options.init, 47 | }); 48 | await atomicalBuilder.setData(filesData); 49 | 50 | // Attach any requested bitwork 51 | if (this.options.bitworkc) { 52 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 53 | } 54 | 55 | // Add the atomical to update 56 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 57 | 58 | // The receiver output 59 | atomicalBuilder.addOutput({ 60 | address: this.owner.address, 61 | value: this.options.satsoutput as any || 1000// todo: determine how to auto detect the total input and set it to that 62 | }); 63 | 64 | const result = await atomicalBuilder.start(this.funding.WIF); 65 | return { 66 | success: true, 67 | data: result 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /lib/commands/set-relation-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { getAndCheckAtomicalInfo, logBanner } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | 15 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 16 | initEccLib(tinysecp as any); 17 | 18 | export class SetRelationInteractiveCommand implements CommandInterface { 19 | constructor( 20 | private electrumApi: ElectrumApiInterface, 21 | private options: BaseRequestOptions, 22 | private atomicalId: string, 23 | private relationName: string, 24 | private values: string[], 25 | private owner: IWalletRecord, 26 | private funding: IWalletRecord, 27 | ) { 28 | 29 | } 30 | async run(): Promise { 31 | logBanner(`Set Relation Interactive`); 32 | const { atomicalInfo, locationInfo, inputUtxoPartial } = await getAndCheckAtomicalInfo(this.electrumApi, this.atomicalId, this.owner.address); 33 | const atomicalBuilder = new AtomicalOperationBuilder({ 34 | electrumApi: this.electrumApi, 35 | rbf: this.options.rbf, 36 | satsbyte: this.options.satsbyte, 37 | address: this.owner.address, 38 | disableMiningChalk: this.options.disableMiningChalk, 39 | opType: 'mod', 40 | nftOptions: { 41 | satsoutput: this.options.satsoutput as any 42 | }, 43 | meta: this.options.meta, 44 | ctx: this.options.ctx, 45 | init: this.options.init, 46 | }); 47 | 48 | await atomicalBuilder.setData({ 49 | $path: '/relns', 50 | [this.relationName]: this.values 51 | }); 52 | 53 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 54 | 55 | // The receiver output 56 | atomicalBuilder.addOutput({ 57 | address: this.owner.address, 58 | value: this.options.satsoutput as any || 1000 59 | }); 60 | const result = await atomicalBuilder.start(this.funding.WIF); 61 | return { 62 | success: true, 63 | data: result 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /lib/commands/splat-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { logBanner } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | import { GetAtomicalsAtLocationCommand } from "./get-atomicals-at-location-command"; 15 | import { GetUtxoPartialFromLocation } from "../utils/address-helpers"; 16 | import { IInputUtxoPartial } from "../types/UTXO.interface"; 17 | import { hasAtomicalType } from "../utils/atomical-format-helpers"; 18 | 19 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 20 | initEccLib(tinysecp as any); 21 | 22 | export class SplatInteractiveCommand implements CommandInterface { 23 | constructor( 24 | private electrumApi: ElectrumApiInterface, 25 | private options: BaseRequestOptions, 26 | private locationId: string, 27 | private owner: IWalletRecord, 28 | private funding: IWalletRecord, 29 | ) { 30 | } 31 | async run(): Promise { 32 | logBanner(`Splat Interactive`); 33 | const command: CommandInterface = new GetAtomicalsAtLocationCommand(this.electrumApi, this.locationId); 34 | const response: any = await command.run(); 35 | 36 | if (!response || !response.success) { 37 | throw new Error(response); 38 | } 39 | const atomicals = response.data.atomicals; 40 | const atomicalNfts = atomicals.filter((item) => { 41 | return item.type === 'NFT'; 42 | }); 43 | if (atomicalNfts.length <= 1) { 44 | throw new Error('Multiple NFTs were not found at the same location. Nothing to skip split out.'); 45 | } 46 | const hasFts = hasAtomicalType('FT', atomicals); 47 | if (hasFts) { 48 | throw new Error('Splat operation attempted for a location which contains non-NFT type atomicals. Detected FT type. Use Split operation first. Aborting...'); 49 | } 50 | 51 | const inputUtxoPartial: IInputUtxoPartial | any = GetUtxoPartialFromLocation(this.owner.address, response.data.location_info); 52 | const atomicalBuilder = new AtomicalOperationBuilder({ 53 | electrumApi: this.electrumApi, 54 | rbf: this.options.rbf, 55 | satsbyte: this.options.satsbyte, 56 | address: this.owner.address, 57 | disableMiningChalk: this.options.disableMiningChalk, 58 | opType: 'x', 59 | splatOptions: { 60 | satsoutput: this.options.satsoutput as any 61 | }, 62 | meta: this.options.meta, 63 | ctx: this.options.ctx, 64 | init: this.options.init, 65 | }); 66 | 67 | // Add the owner of the atomicals at the location 68 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 69 | // ... and make sure to assign outputs to capture each atomical splatted out 70 | let amountSkipped = 0; 71 | atomicalBuilder.addOutput({ 72 | address: inputUtxoPartial.address, 73 | value: inputUtxoPartial.witnessUtxo.value 74 | }); 75 | if (this.options.bitworkc) { 76 | atomicalBuilder.setBitworkCommit(this.options.bitworkc); 77 | } 78 | amountSkipped += inputUtxoPartial.witnessUtxo.value; 79 | for (const nft of atomicalNfts) { 80 | // We do not actually need to know which atomical it is, just that we create an output for each 81 | // Make sure to make N outputs, for each atomical NFT 82 | atomicalBuilder.addOutput({ 83 | address: inputUtxoPartial.address, 84 | value: this.options.satsoutput as any || 1000 85 | }); 86 | } 87 | const result = await atomicalBuilder.start(this.funding.WIF); 88 | return { 89 | success: true, 90 | data: result 91 | } 92 | } 93 | } 94 | 95 | 96 | -------------------------------------------------------------------------------- /lib/commands/split-interactive-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { TinySecp256k1Interface } from 'ecpair'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | bitcoin.initEccLib(ecc); 7 | import { 8 | initEccLib, 9 | } from "bitcoinjs-lib"; 10 | import { logBanner } from "./command-helpers"; 11 | import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder"; 12 | import { BaseRequestOptions } from "../interfaces/api.interface"; 13 | import { IWalletRecord } from "../utils/validate-wallet-storage"; 14 | import { GetAtomicalsAtLocationCommand } from "./get-atomicals-at-location-command"; 15 | import { GetUtxoPartialFromLocation } from "../utils/address-helpers"; 16 | import { IInputUtxoPartial } from "../types/UTXO.interface"; 17 | import { hasAtomicalType, isAtomicalId } from "../utils/atomical-format-helpers"; 18 | 19 | const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); 20 | initEccLib(tinysecp as any); 21 | 22 | export class SplitInteractiveCommand implements CommandInterface { 23 | constructor( 24 | private electrumApi: ElectrumApiInterface, 25 | private options: BaseRequestOptions, 26 | private locationId: string, 27 | private owner: IWalletRecord, 28 | private funding: IWalletRecord, 29 | ) { 30 | } 31 | async run(): Promise { 32 | logBanner(`Split FTs Interactive`); 33 | const command: CommandInterface = new GetAtomicalsAtLocationCommand(this.electrumApi, this.locationId); 34 | const response: any = await command.run(); 35 | if (!response || !response.success) { 36 | throw new Error(response); 37 | } 38 | const atomicals = response.data.atomicals; 39 | const atomicalFts = atomicals.filter((item) => { 40 | return item.type === 'FT'; 41 | }); 42 | 43 | console.log('Found multiple FTs at the same location: ', atomicalFts); 44 | 45 | const hasNfts = hasAtomicalType('NFT', atomicals); 46 | if (hasNfts) { 47 | console.log('Found at least one NFT at the same location. The first output will contain the NFTs, and the second output, etc will contain the FTs split out. After you may use the splat command to separate multiple NFTs if they exist at the same location.') 48 | } 49 | 50 | if (!hasNfts && atomicalFts.length <= 1) { 51 | throw new Error('Multiple FTs were not found at the same location. Nothing to skip split.'); 52 | } 53 | 54 | const inputUtxoPartial: IInputUtxoPartial | any = GetUtxoPartialFromLocation(this.owner.address, response.data.location_info); 55 | const atomicalBuilder = new AtomicalOperationBuilder({ 56 | electrumApi: this.electrumApi, 57 | rbf: this.options.rbf, 58 | satsbyte: this.options.satsbyte, 59 | address: this.owner.address, 60 | disableMiningChalk: this.options.disableMiningChalk, 61 | opType: 'y', 62 | skipOptions: { 63 | }, 64 | meta: this.options.meta, 65 | ctx: this.options.ctx, 66 | init: this.options.init, 67 | }); 68 | 69 | // Add the owner of the atomicals at the location 70 | atomicalBuilder.addInputUtxo(inputUtxoPartial, this.owner.WIF) 71 | // ... and make sure to assign outputs to capture each atomical split 72 | const ftsToSplit: any = {} 73 | let amountSkipped = 0; 74 | if (hasNfts) { 75 | atomicalBuilder.addOutput({ 76 | address: inputUtxoPartial.address, 77 | value: inputUtxoPartial.witnessUtxo.value 78 | }); 79 | amountSkipped += inputUtxoPartial.witnessUtxo.value; 80 | } 81 | if (isNaN(amountSkipped)) { 82 | throw new Error('Critical error amountSkipped isNaN'); 83 | } 84 | for (const ft of atomicalFts) { 85 | if (!ft.atomical_id) { 86 | throw new Error('Critical error atomical_id not set for FT'); 87 | } 88 | if (!isAtomicalId(ft.atomical_id)) { 89 | throw new Error('Critical error atomical_id is not valid for FT'); 90 | } 91 | // Make sure to make N outputs, for each atomical NFT 92 | ftsToSplit[ft.atomical_id] = amountSkipped; 93 | atomicalBuilder.addOutput({ 94 | address: inputUtxoPartial.address, 95 | value: inputUtxoPartial.witnessUtxo.value 96 | }); 97 | // Add the amount to skip for the next FT 98 | amountSkipped += inputUtxoPartial.witnessUtxo.value; 99 | if (isNaN(amountSkipped)) { 100 | throw new Error('Critical error amountSkipped isNaN'); 101 | } 102 | } 103 | await atomicalBuilder.setData(ftsToSplit); 104 | const result = await atomicalBuilder.start(this.funding.WIF); 105 | return { 106 | success: true, 107 | data: result 108 | } 109 | } 110 | } 111 | 112 | 113 | -------------------------------------------------------------------------------- /lib/commands/summary-containers-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | 5 | export interface SummaryContainersCommandResultInterface { 6 | success: boolean; 7 | message?: string; 8 | data?: any; 9 | error?: any; 10 | } 11 | 12 | export interface ContainersSummaryItemInterface { 13 | atomical_id: string; 14 | atomical_number: number; 15 | request_full_realm_name: string; 16 | full_realm_name?: string; 17 | status: string; 18 | } 19 | export interface ContainersSummaryItemMapInterface { 20 | [key: string]: ContainersSummaryItemInterface | any; 21 | } 22 | 23 | export class SummaryContainersCommand implements CommandInterface { 24 | constructor( 25 | private electrumApi: ElectrumApiInterface, 26 | private address: string, 27 | private filter?: string 28 | ) { 29 | } 30 | 31 | async run(): Promise { 32 | const { scripthash } = detectAddressTypeToScripthash(this.address); 33 | let res = await this.electrumApi.atomicalsByScripthash(scripthash, true); 34 | const statusMap: ContainersSummaryItemMapInterface = {} 35 | 36 | for (const prop in res.atomicals) { 37 | if (!res.atomicals.hasOwnProperty(prop)) { 38 | continue; 39 | } 40 | const entry = res.atomicals[prop]; 41 | if (entry.type !== 'NFT') { 42 | continue; 43 | } 44 | if (!entry.subtype || (entry.subtype !== 'container' && entry.subtype !== 'request_container')) { 45 | continue; 46 | } 47 | 48 | const entryStatus = entry['request_container_status']['status']; 49 | 50 | if (this.filter) { 51 | const myRe = new RegExp(this.filter); 52 | if (!myRe.test(entryStatus)) { 53 | continue; 54 | } 55 | } 56 | statusMap[entry.subtype] = statusMap[entry.subtype] || {} 57 | statusMap[entry.subtype][entryStatus] = statusMap[entry.subtype][entryStatus] || [] 58 | statusMap[entry.subtype][entryStatus].push({ 59 | atomical_id: entry['atomical_id'], 60 | atomical_number: entry['atomical_number'], 61 | request_container: entry['request_container'], 62 | status: entry['request_container_status'] 63 | }) 64 | } 65 | return { 66 | success: true, 67 | data: { 68 | ...statusMap, 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /lib/commands/summary-realms-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | 5 | export interface SummaryRealmsCommandResultInterface { 6 | success: boolean; 7 | message?: string; 8 | data?: any; 9 | error?: any; 10 | } 11 | 12 | export interface RealmsSummaryItemInterface { 13 | atomical_id: string; 14 | atomical_number: number; 15 | request_full_realm_name: string; 16 | full_realm_name?: string; 17 | status: string; 18 | } 19 | export interface RealmsSummaryItemMapInterface { 20 | [key: string]: RealmsSummaryItemInterface | any; 21 | } 22 | 23 | export class SummaryRealmsCommand implements CommandInterface { 24 | constructor( 25 | private electrumApi: ElectrumApiInterface, 26 | private address: string, 27 | private filter?: string 28 | ) { 29 | } 30 | 31 | async run(): Promise { 32 | const { scripthash } = detectAddressTypeToScripthash(this.address); 33 | let res = await this.electrumApi.atomicalsByScripthash(scripthash, true); 34 | const statusMap: RealmsSummaryItemMapInterface = {} 35 | 36 | for (const prop in res.atomicals) { 37 | if (!res.atomicals.hasOwnProperty(prop)) { 38 | continue; 39 | } 40 | const entry = res.atomicals[prop]; 41 | if (entry.type !== 'NFT') { 42 | continue; 43 | } 44 | if (!entry.subtype || (entry.subtype !== 'realm' && entry.subtype !== 'request_realm')) { 45 | continue; 46 | } 47 | const entryStatus = entry['request_realm_status']['status']; 48 | 49 | if (this.filter) { 50 | const myRe = new RegExp(this.filter); 51 | if (!myRe.test(entryStatus)) { 52 | continue; 53 | } 54 | } 55 | 56 | statusMap[entry.subtype] = statusMap[entry.subtype] || {} 57 | statusMap[entry.subtype][entryStatus] = statusMap[entry.subtype][entryStatus] || [] 58 | statusMap[entry.subtype][entryStatus].push({ 59 | atomical_id: entry['atomical_id'], 60 | atomical_number: entry['atomical_number'], 61 | request_realm: entry['request_realm'], 62 | status: entry['request_realm_status'] 63 | }) 64 | } 65 | return { 66 | success: true, 67 | data: { 68 | ...statusMap, 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /lib/commands/summary-subrealms-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | 5 | export interface SummarySubrealmsCommandResultInterface { 6 | success: boolean; 7 | message?: string; 8 | data?: any; 9 | error?: any; 10 | } 11 | 12 | export interface SubrealmSummaryItemInterface { 13 | atomical_id: string; 14 | atomical_number: number; 15 | request_full_realm_name: string; 16 | full_realm_name?: string; 17 | status: string; 18 | } 19 | export interface SubrealmSummaryItemMapInterface { 20 | [key: string]: SubrealmSummaryItemInterface | any; 21 | } 22 | 23 | export class SummarySubrealmsCommand implements CommandInterface { 24 | constructor( 25 | private electrumApi: ElectrumApiInterface, 26 | private address: string, 27 | private filter?: string 28 | ) { 29 | } 30 | 31 | async run(): Promise { 32 | const { scripthash } = detectAddressTypeToScripthash(this.address); 33 | let res = await this.electrumApi.atomicalsByScripthash(scripthash, true); 34 | const statusMap: SubrealmSummaryItemMapInterface = {} 35 | 36 | for (const prop in res.atomicals) { 37 | if (!res.atomicals.hasOwnProperty(prop)) { 38 | continue; 39 | } 40 | const entry = res.atomicals[prop]; 41 | if (entry.type !== 'NFT') { 42 | continue; 43 | } 44 | if (!entry.subtype || (entry.subtype !== 'subrealm' && entry.subtype !== 'request_subrealm')) { 45 | continue; 46 | } 47 | const entryStatus = entry['request_subrealm_status']['status']; 48 | 49 | if (this.filter) { 50 | const myRe = new RegExp(this.filter); 51 | if (!myRe.test(entryStatus)) { 52 | continue; 53 | } 54 | } 55 | 56 | statusMap[entry.subtype] = statusMap[entry.subtype] || {} 57 | statusMap[entry.subtype][entryStatus] = statusMap[entry.subtype][entryStatus] || [] 58 | statusMap[entry.subtype][entryStatus].push({ 59 | atomical_id: entry['atomical_id'], 60 | atomical_number: entry['atomical_number'], 61 | full_realm_name: entry['full_realm_name'], 62 | request_full_realm_name: entry['request_full_realm_name'], 63 | status: entry['request_subrealm_status'] 64 | }) 65 | } 66 | return { 67 | success: true, 68 | data: { 69 | ...statusMap, 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /lib/commands/summary-tickers-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | 5 | export interface SummaryTickersCommandResultInterface { 6 | success: boolean; 7 | message?: string; 8 | data?: any; 9 | error?: any; 10 | } 11 | 12 | export interface TickersSummaryItemInterface { 13 | atomical_id: string; 14 | atomical_number: number; 15 | ticker?: string; 16 | status: string; 17 | } 18 | export interface TickersSummaryItemMapInterface { 19 | [key: string]: TickersSummaryItemInterface | any; 20 | } 21 | 22 | export class SummaryTickersCommand implements CommandInterface { 23 | constructor( 24 | private electrumApi: ElectrumApiInterface, 25 | private address: string, 26 | private filter?: string 27 | ) { 28 | } 29 | 30 | async run(): Promise { 31 | const { scripthash } = detectAddressTypeToScripthash(this.address); 32 | let res = await this.electrumApi.atomicalsByScripthash(scripthash, true); 33 | const statusMap: TickersSummaryItemMapInterface = {} 34 | 35 | for (const prop in res.atomicals) { 36 | if (!res.atomicals.hasOwnProperty(prop)) { 37 | continue; 38 | } 39 | const entry = res.atomicals[prop]; 40 | if (entry.type !== 'FT') { 41 | continue; 42 | } 43 | if (!entry['request_ticker_status']) { 44 | continue; 45 | } 46 | 47 | const entryStatus = entry['request_ticker_status']['status']; 48 | 49 | if (this.filter) { 50 | const myRe = new RegExp(this.filter); 51 | if (!myRe.test(entryStatus)) { 52 | continue; 53 | } 54 | } 55 | 56 | statusMap[entry.subtype] = statusMap[entry.subtype] || {} 57 | statusMap[entry.subtype][entryStatus] = statusMap[entry.subtype][entryStatus] || [] 58 | statusMap[entry.subtype][entryStatus].push({ 59 | atomical_id: entry['atomical_id'], 60 | atomical_number: entry['atomical_number'], 61 | ticker: entry['ticker'], 62 | confirmed: entry['confirmed'], 63 | status: entry['request_ticker_status'], 64 | request_ticker: entry['request_ticker'], 65 | }) 66 | } 67 | return { 68 | success: true, 69 | data: { 70 | ...statusMap, 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /lib/commands/tx-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | 4 | export class TxCommand implements CommandInterface { 5 | constructor( 6 | private electrumApi: ElectrumApiInterface, 7 | private txid: string, 8 | ) { 9 | } 10 | 11 | async run(): Promise { 12 | return this.electrumApi.getTx(this.txid); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/commands/wallet-create-command.ts: -------------------------------------------------------------------------------- 1 | import { CommandResultInterface } from "./command-result.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { createPrimaryAndFundingImportedKeyPairs } from "../utils/create-key-pair"; 4 | 5 | export class WalletCreateCommand implements CommandInterface { 6 | async run(): Promise { 7 | const keypairs = await createPrimaryAndFundingImportedKeyPairs(); 8 | 9 | return { 10 | success: true, 11 | data: keypairs 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /lib/commands/wallet-import-command.ts: -------------------------------------------------------------------------------- 1 | import { CommandResultInterface } from "./command-result.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { toXOnly } from "../utils/create-key-pair"; 4 | import { jsonFileExists, jsonFileReader, jsonFileWriter } from "../utils/file-utils"; 5 | import { IValidatedWalletInfo } from "../utils/validate-wallet-storage"; 6 | const bitcoin = require('bitcoinjs-lib'); 7 | import ECPairFactory from 'ecpair'; 8 | import * as ecc from 'tiny-secp256k1'; 9 | import { walletPathResolver } from "../utils/wallet-path-resolver"; 10 | import { NETWORK } from "./command-helpers"; 11 | 12 | bitcoin.initEccLib(ecc); 13 | const ECPair = ECPairFactory(ecc); 14 | 15 | const walletPath = walletPathResolver(); 16 | 17 | export class WalletImportCommand implements CommandInterface { 18 | 19 | constructor(private wif: string, private alias: string) { 20 | } 21 | async run(): Promise { 22 | if (!(await this.walletExists())) { 23 | throw "wallet.json does NOT exist, please create one first with wallet-init" 24 | } 25 | const walletFileData: IValidatedWalletInfo = (await jsonFileReader(walletPath)) as IValidatedWalletInfo; 26 | if (!walletFileData.imported) { 27 | walletFileData.imported = {}; 28 | } 29 | if (walletFileData.imported.hasOwnProperty(this.alias)) { 30 | throw `Wallet alias ${this.alias} already exists!` 31 | } 32 | // Just make a backup for now to be safe 33 | await jsonFileWriter(walletPath + '.' + (new Date()).getTime() + '.walletbackup', walletFileData); 34 | 35 | // Get the wif and the address and ensure they match 36 | const importedKeypair = ECPair.fromWIF(this.wif); 37 | const { address, output } = bitcoin.payments.p2tr({ 38 | internalPubkey: toXOnly(importedKeypair.publicKey), 39 | network: NETWORK 40 | }); 41 | const walletImportedField = Object.assign({}, walletFileData.imported, { 42 | [this.alias]: { 43 | address, 44 | WIF: this.wif 45 | } 46 | }); 47 | walletFileData['imported'] = walletImportedField; 48 | await jsonFileWriter(walletPath, walletFileData); 49 | return { 50 | success: true, 51 | data: { 52 | address, 53 | alias: this.alias 54 | } 55 | } 56 | } 57 | 58 | async walletExists() { 59 | if (await jsonFileExists(walletPath)) { 60 | return true; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/commands/wallet-info-command.ts: -------------------------------------------------------------------------------- 1 | import { ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { detectAddressTypeToScripthash } from "../utils/address-helpers"; 4 | 5 | export class WalletInfoCommand implements CommandInterface { 6 | constructor( 7 | private electrumApi: ElectrumApiInterface, 8 | private address: string, 9 | private verbose: boolean 10 | ) { 11 | } 12 | 13 | async run(): Promise { 14 | const { scripthash } = detectAddressTypeToScripthash(this.address); 15 | let res = await this.electrumApi.atomicalsByScripthash(scripthash, true); 16 | let history = undefined; 17 | if (this.verbose) { 18 | history = await this.electrumApi.history(scripthash); 19 | } 20 | const plainUtxos: any[] = []; 21 | let total_confirmed = 0; 22 | let total_unconfirmed = 0; 23 | let regular_confirmed = 0; 24 | let regular_unconfirmed = 0; 25 | let atomicals_confirmed = 0; 26 | let atomicals_unconfirmed = 0; 27 | const atomicalsUtxos: any[] = []; 28 | 29 | for (const utxo of res.utxos) { 30 | 31 | if (utxo.height <= 0) { 32 | total_unconfirmed += utxo.value; 33 | 34 | } else { 35 | total_confirmed += utxo.value; 36 | } 37 | 38 | if (utxo.atomicals && utxo.atomicals.length) { 39 | 40 | if (utxo.height <= 0) { 41 | atomicals_unconfirmed += utxo.value; 42 | } else { 43 | 44 | atomicals_confirmed += utxo.value; 45 | } 46 | atomicalsUtxos.push(utxo); 47 | continue; 48 | } 49 | 50 | if (utxo.height <= 0) { 51 | regular_unconfirmed += utxo.value; 52 | } else { 53 | 54 | regular_confirmed += utxo.value; 55 | } 56 | 57 | plainUtxos.push(utxo); 58 | } 59 | 60 | 61 | return { 62 | success: true, 63 | data: { 64 | address: this.address, 65 | scripthash: scripthash, 66 | atomicals_count: Object.keys(res.atomicals).length, 67 | atomicals_utxos: atomicalsUtxos, 68 | atomicals_balances: res.atomicals, 69 | total_confirmed, 70 | total_unconfirmed, 71 | atomicals_confirmed, 72 | atomicals_unconfirmed, 73 | regular_confirmed, 74 | regular_unconfirmed, 75 | regular_utxos: plainUtxos, 76 | regular_utxo_count: plainUtxos.length, 77 | history 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /lib/commands/wallet-init-command.ts: -------------------------------------------------------------------------------- 1 | import { CommandResultInterface } from "./command-result.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { createPrimaryAndFundingImportedKeyPairs } from "../utils/create-key-pair"; 4 | import { jsonFileExists, jsonFileWriter } from "../utils/file-utils"; 5 | import { walletPathResolver } from "../utils/wallet-path-resolver"; 6 | import * as fs from 'fs'; 7 | 8 | const walletPath = walletPathResolver(); 9 | 10 | export class WalletInitCommand implements CommandInterface { 11 | constructor( 12 | private phrase: string | undefined, 13 | private path: string, 14 | private passphrase?: string, 15 | private n?: number 16 | ) { 17 | } 18 | 19 | async run(): Promise { 20 | if (await this.walletExists()) { 21 | throw "wallet.json exists, please remove it first to initialize another wallet. You may also use 'wallet-create' command to generate a new wallet." 22 | } 23 | 24 | const { 25 | wallet, 26 | imported 27 | } = await createPrimaryAndFundingImportedKeyPairs( 28 | this.phrase, 29 | this.path, 30 | this.passphrase, 31 | this.n 32 | ); 33 | const walletDir = `wallets/`; 34 | if (!fs.existsSync(walletDir)) { 35 | fs.mkdirSync(walletDir); 36 | } 37 | const created = { 38 | phrase: wallet.phrase, 39 | passphrase: wallet.passphrase, 40 | primary: { 41 | address: wallet.primary.address, 42 | path: wallet.primary.path, 43 | WIF: wallet.primary.WIF 44 | }, 45 | funding: { 46 | address: wallet.funding.address, 47 | path: wallet.funding.path, 48 | WIF: wallet.funding.WIF 49 | }, 50 | imported 51 | }; 52 | await jsonFileWriter(walletPath, created); 53 | return { 54 | success: true, 55 | data: created 56 | } 57 | } 58 | async walletExists() { 59 | if (await jsonFileExists(walletPath)) { 60 | return true; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/commands/wallet-phrase-decode-command.ts: -------------------------------------------------------------------------------- 1 | import { CommandResultInterface } from "./command-result.interface"; 2 | import { CommandInterface } from "./command.interface"; 3 | import { decodeMnemonicPhrase } from "../utils/decode-mnemonic-phrase"; 4 | 5 | export class WalletPhraseDecodeCommand implements CommandInterface { 6 | constructor(private phrase: string, private path: string, private passphrase?: string) { 7 | } 8 | async run(): Promise { 9 | const wallet = await decodeMnemonicPhrase(this.phrase, this.path, this.passphrase); 10 | return { 11 | success: true, 12 | data: wallet 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/commands/witness_stack_to_script_witness.ts: -------------------------------------------------------------------------------- 1 | //import varuint from "varuint-bitcoin"; 2 | import * as varuint from 'varuint-bitcoin'; 3 | /** 4 | * Helper function that produces a serialized witness script 5 | * https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/csv.spec.ts#L477 6 | */ 7 | export function witnessStackToScriptWitness(witness: Buffer[]) { 8 | let buffer = Buffer.allocUnsafe(0) 9 | 10 | function writeSlice(slice: Buffer) { 11 | buffer = Buffer.concat([buffer, Buffer.from(slice)]) 12 | } 13 | 14 | function writeVarInt(i: number) { 15 | const currentLen = buffer.length; 16 | const varintLen = varuint.encodingLength(i) 17 | 18 | buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]) 19 | varuint.encode(i, buffer, currentLen) 20 | } 21 | 22 | function writeVarSlice(slice: Buffer) { 23 | writeVarInt(slice.length) 24 | writeSlice(slice) 25 | } 26 | 27 | function writeVector(vector: Buffer[]) { 28 | writeVarInt(vector.length) 29 | vector.forEach(writeVarSlice) 30 | } 31 | 32 | writeVector(witness) 33 | 34 | return buffer 35 | } -------------------------------------------------------------------------------- /lib/errors/Errors.ts: -------------------------------------------------------------------------------- 1 | export class InvalidNameError extends Error { 2 | constructor(m?: string) { 3 | super(m); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/interfaces/atomical-file-data.ts: -------------------------------------------------------------------------------- 1 | export interface AtomicalFileData { 2 | name: string; 3 | contentType: string; // If it's 'object' then it will be treated as raw json 4 | data: Buffer | any; 5 | } -------------------------------------------------------------------------------- /lib/interfaces/atomical-status.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Location { 2 | location: string; 3 | txid: string; 4 | index: number; 5 | scripthash: string; 6 | value: number; 7 | script: string; 8 | address?: string; 9 | atomicals_at_location?: any[], 10 | tx_num?: number, 11 | adddress?: string; 12 | } 13 | 14 | export interface LocationInfo { 15 | locations: Location[] 16 | } 17 | 18 | export interface MintInfo { 19 | commit_txid: string; 20 | commit_index: number; 21 | commit_location: string; 22 | commit_tx_num: number; 23 | commit_height: number; 24 | reveal_location_txid: string; 25 | reveal_location_index: number; 26 | reveal_location: string; 27 | reveal_location_tx_num: number; 28 | reveal_location_height: number; 29 | reveal_location_header: string; 30 | reveal_location_blockhash: string; 31 | reveal_location_scripthash: string; 32 | reveal_location_script: string; 33 | reveal_location_value: number; 34 | args?: { [key: string]: any }; 35 | meta?: { [key: string]: any }; 36 | ctx?: { [key: string]: any }; 37 | init?: { [key: string]: any }; 38 | reveal_location_address?: string; 39 | blockheader_info?: { 40 | version?: number, 41 | prevHash?: string; 42 | merkleRoot?: string; 43 | timestamp?: number; 44 | bits?: number; 45 | nonce?: number; 46 | }; 47 | $request_realm?: string; 48 | $request_subrealm?: string; 49 | $request_container?: string; 50 | $request_ticker?: string; 51 | $pid?: string; 52 | $bitwork?: { 53 | $bitworkc?: string; 54 | $bitworkr?: string; 55 | } 56 | } 57 | 58 | export interface MintDataSummary { 59 | fields: { [key: string]: any }; 60 | } 61 | 62 | export interface StateInfo { 63 | } 64 | 65 | export interface RuleSet { 66 | pattern: string; 67 | outputs: Array<{ 68 | v: number, 69 | s: string, 70 | }> 71 | } 72 | 73 | export interface ApplicableRule { 74 | rule_set_txid: string; 75 | rule_set_height: number; 76 | rule_valid_from_height: number; 77 | matched_rule: RuleSet 78 | } 79 | 80 | export interface SubrealmCandidate { 81 | tx_num: number; 82 | atomical_id: string; 83 | txid: string; 84 | commit_height: number; 85 | reveal_location_height: number; 86 | payment?: string; 87 | payment_type: string; 88 | make_payment_from_height: number; 89 | payment_due_no_later_than_height: string; 90 | applicable_rule?: ApplicableRule; 91 | } 92 | 93 | export interface RequestSubrealmStatus { 94 | status: 'verified' | 95 | 'expired_revealed_late' | 96 | 'expired_payment_not_received' | 97 | 'claimed_by_other' | 98 | 'invalid_request_subrealm_no_matched_applicable_rule' | 99 | 'pending_awaiting_confirmations_payment_received_prematurely' | 100 | 'pending_awaiting_confirmations_for_payment_window' | 101 | 'pending_awaiting_confirmations' | 102 | 'pending_awaiting_payment' | 103 | string; 104 | verified_atomical_id?: string; 105 | claimed_by_atomical_id?: string; 106 | pending_candidate_atomical_id?: string; 107 | pending_claimed_by_atomical_id?: string; 108 | note?: string; 109 | } 110 | 111 | export interface RequestNameStatus { 112 | status: 'verified' | 'expired_revealed_late' | 'claimed_by_other' | 'pending_candidate' | 'pending_claimed_by_other' | string; 113 | verified_atomical_id?: string; 114 | claimed_by_atomical_id?: string; 115 | pending_candidate_atomical_id?: string; 116 | note?: string; 117 | } 118 | 119 | export interface NameCandidate { 120 | tx_num: number; 121 | atomical_id: string; 122 | txid: string; 123 | commit_height: number; 124 | reveal_location_height: number; 125 | } 126 | 127 | export interface AtomicalStatus { 128 | atomical_id: string; 129 | atomical_number: number; 130 | type: 'NFT' | 'FT'; 131 | subtype?: 'request_realm' | 'realm' | 'request_subrealm' | 'subrealm' | 'request_container' | 'container' | 'direct' | 'decentralized'; 132 | location_info_obj?: LocationInfo; 133 | mint_info?: MintInfo; 134 | mint_data?: MintDataSummary; 135 | state_info?: StateInfo; 136 | // Relationships 137 | $relns?: { [key: string]: any }; 138 | // Bitwork proof of work 139 | $bitwork?: { 140 | $bitworkc?: string; 141 | $bitworkr?: string; 142 | }; 143 | // realms 144 | $request_realm_status?: RequestNameStatus; 145 | $realm_candidates?: NameCandidate[]; 146 | $request_realm?: string; 147 | $realm?: string; 148 | // Subrealm 149 | $full_realm_name?: string; // applies to realms and subrealms both 150 | $request_full_realm_name?: string; 151 | $subrealm_candidates?: SubrealmCandidate[]; 152 | $request_subrealm_status?: RequestSubrealmStatus; 153 | $request_subrealm?: string; 154 | $pid?: string; 155 | $subrealm?: string; 156 | // tickers 157 | $max_supply?: number; 158 | $mint_height?: number; 159 | $mint_amount?: number; 160 | $max_mints?: number; 161 | $mint_bitworkc?: string; 162 | $mint_bitworkr?: string; 163 | $ticker_candidates?: NameCandidate[] 164 | $request_ticker_status?: RequestNameStatus; 165 | $request_ticker?: string; 166 | $ticker?: string; 167 | // containers 168 | $request_container_status?: RequestNameStatus; 169 | $container_candidates?: NameCandidate[]; 170 | $request_container?: string; 171 | $container?: string; 172 | } -------------------------------------------------------------------------------- /lib/interfaces/configuration.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ConfigurationInterface { 3 | electrumxWebsocketUrl: string; 4 | } 5 | 6 | export interface HydratedConfigurationInterface { 7 | electrumxWebsocketUrl: string 8 | } -------------------------------------------------------------------------------- /lib/interfaces/filemap.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FileMap { 2 | [inputIndex: string]: { 3 | directory: string; 4 | files: { 5 | [fileName: string]: { 6 | fileName: string; 7 | fileNameWithExtension: string; 8 | detectedExtension: string; 9 | fullPath: string; 10 | contentType: string; 11 | contentLength: number; 12 | body: any; 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /lib/types/UTXO.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UTXO { 2 | txid: string; 3 | txId: string; 4 | index: number; 5 | vout: number; 6 | value: number; 7 | script?: string; 8 | height?: number; 9 | outputIndex: number; 10 | atomicals: { 11 | [atomical_id: string]: string 12 | }; 13 | atomicals_at_location?: any[]; 14 | nonWitnessUtxo?: Buffer; 15 | } 16 | 17 | export interface BalanceData { 18 | balance: string; 19 | unconfirmed: number; 20 | confirmed: number; 21 | utxos: UTXO[]; 22 | } 23 | 24 | export interface IInputUtxoPartial { 25 | hash: string; 26 | index: number; 27 | address: string; 28 | witnessUtxo: { 29 | value: number, 30 | script: Buffer, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/types/protocol-tags.ts: -------------------------------------------------------------------------------- 1 | export const ATOMICALS_PROTOCOL_ENVELOPE_ID = 'atom'; 2 | -------------------------------------------------------------------------------- /lib/utils/address-keypair-path.ts: -------------------------------------------------------------------------------- 1 | const bitcoin = require('bitcoinjs-lib'); 2 | import * as ecc from 'tiny-secp256k1'; 3 | bitcoin.initEccLib(ecc); 4 | const bip39 = require('bip39'); 5 | import BIP32Factory from 'bip32'; 6 | import { toXOnly } from './create-key-pair'; 7 | import { NETWORK } from '../commands/command-helpers'; 8 | const bip32 = BIP32Factory(ecc); 9 | 10 | export interface ExtendTaprootAddressScriptKeyPairInfo { 11 | address: string; 12 | tweakedChildNode: any; 13 | childNodeXOnlyPubkey: any; 14 | output: any; 15 | keyPair: any; 16 | path: string; 17 | } 18 | 19 | export const getExtendTaprootAddressKeypairPath = async ( 20 | phrase: string, 21 | path: string, 22 | passphrase?: string, 23 | ): Promise => { 24 | const seed = await bip39.mnemonicToSeed(phrase, passphrase); 25 | const rootKey = bip32.fromSeed(seed); 26 | const childNode = rootKey.derivePath(path); 27 | const childNodeXOnlyPubkey = childNode.publicKey.slice(1, 33); 28 | // This is new for taproot 29 | // Note: we are using mainnet here to get the correct address 30 | // The output is the same no matter what the network is. 31 | const { address, output } = bitcoin.payments.p2tr({ 32 | internalPubkey: childNodeXOnlyPubkey, 33 | network: NETWORK 34 | }); 35 | 36 | // Used for signing, since the output and address are using a tweaked key 37 | // We must tweak the signer in the same way. 38 | const tweakedChildNode = childNode.tweak( 39 | bitcoin.crypto.taggedHash('TapTweak', childNodeXOnlyPubkey), 40 | ); 41 | 42 | return { 43 | address, 44 | tweakedChildNode, 45 | childNodeXOnlyPubkey, 46 | output, 47 | keyPair: childNode, 48 | path, 49 | } 50 | } 51 | 52 | export interface KeyPairInfo { 53 | address: string; 54 | output: string; 55 | childNodeXOnlyPubkey: any; 56 | tweakedChildNode: any; 57 | childNode: any; 58 | } 59 | 60 | export const getKeypairInfo = (childNode: any): KeyPairInfo => { 61 | const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); 62 | // This is new for taproot 63 | // Note: we are using mainnet here to get the correct address 64 | // The output is the same no matter what the network is. 65 | const { address, output } = bitcoin.payments.p2tr({ 66 | internalPubkey: childNodeXOnlyPubkey, 67 | network: NETWORK 68 | }); 69 | 70 | // Used for signing, since the output and address are using a tweaked key 71 | // We must tweak the signer in the same way. 72 | const tweakedChildNode = childNode.tweak( 73 | bitcoin.crypto.taggedHash('TapTweak', childNodeXOnlyPubkey), 74 | ); 75 | 76 | return { 77 | address, 78 | tweakedChildNode, 79 | childNodeXOnlyPubkey, 80 | output, 81 | childNode 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /lib/utils/container-validator.ts: -------------------------------------------------------------------------------- 1 | // A trait 2 | export const validateTrait = (trait) => { 3 | if (!trait) { 4 | return false; 5 | } 6 | } 7 | export const validateContainerTraits = (traits) => { 8 | if (!Array.isArray(traits)) { 9 | return false; 10 | } 11 | for (const trait of traits) { 12 | if (!validateTrait(trait)) { 13 | return false; 14 | } 15 | } 16 | return false; 17 | } 18 | 19 | export const validateContainerItems = (items) => { 20 | 21 | return false; 22 | } 23 | 24 | export const validateContainerItemsForDmint = (dmint) => { 25 | 26 | return false; 27 | } 28 | 29 | export const validateContainerMetadata = (meta) => { 30 | 31 | return false; 32 | } 33 | 34 | export const validateContainerAll = (data) => { 35 | if (!data['traits']) { 36 | return false; 37 | } 38 | if (!data['meta']) { 39 | return false; 40 | } 41 | if (!data['items']) { 42 | return false; 43 | } 44 | if (!data['dmint']) { 45 | return false; 46 | } 47 | return validateContainerTraits(data['traits']) && 48 | validateContainerTraits(data['meta']) && 49 | validateContainerTraits(data['items']) && 50 | validateContainerTraits(data['dmint']); 51 | } 52 | -------------------------------------------------------------------------------- /lib/utils/create-key-pair.ts: -------------------------------------------------------------------------------- 1 | const bitcoin = require('bitcoinjs-lib'); 2 | import * as ecc from 'tiny-secp256k1'; 3 | bitcoin.initEccLib(ecc); 4 | 5 | import ECPairFactory from 'ecpair'; 6 | import { defaultDerivedPath } from './address-helpers'; 7 | import { createMnemonicPhrase } from './create-mnemonic-phrase'; 8 | 9 | const ECPair = ECPairFactory(ecc); 10 | import BIP32Factory from 'bip32'; 11 | import { NETWORK } from '../commands/command-helpers'; 12 | const bip32 = BIP32Factory(ecc); 13 | 14 | export const toXOnly = (publicKey) => { 15 | return publicKey.slice(1, 33); 16 | } 17 | const bip39 = require('bip39'); 18 | 19 | export interface KeyPair { 20 | address: string 21 | publicKey: string 22 | publicKeyXOnly: string 23 | path: string, 24 | WIF: string 25 | privateKey?: string 26 | } 27 | 28 | export const createKeyPair = async ( 29 | phrase: string = '', 30 | path = defaultDerivedPath, 31 | passphrase: string = '' 32 | ) : Promise => { 33 | if (!phrase || phrase === '') { 34 | const phraseResult = createMnemonicPhrase(); 35 | phrase = phraseResult.phrase; 36 | } 37 | const seed = await bip39.mnemonicToSeed(phrase, passphrase); 38 | const rootKey = bip32.fromSeed(seed); 39 | const childNodePrimary = rootKey.derivePath(path); 40 | // const p2pkh = bitcoin.payments.p2pkh({ pubkey: childNodePrimary.publicKey }); 41 | const childNodeXOnlyPubkeyPrimary = toXOnly(childNodePrimary.publicKey); 42 | const p2trPrimary = bitcoin.payments.p2tr({ 43 | internalPubkey: childNodeXOnlyPubkeyPrimary, 44 | network: NETWORK 45 | }); 46 | if (!p2trPrimary.address || !p2trPrimary.output) { 47 | throw "error creating p2tr" 48 | } 49 | /* const p2pkhPrimary = bitcoin.payments.p2pkh({ 50 | pubkey: childNodePrimary.publicKey, 51 | network: NETWORK 52 | }); 53 | // console.log('p2pkhPrimary', p2pkhPrimary, p2pkhPrimary.address.toString()) 54 | */ 55 | // Used for signing, since the output and address are using a tweaked key 56 | // We must tweak the signer in the same way. 57 | const tweakedChildNodePrimary = childNodePrimary.tweak( 58 | bitcoin.crypto.taggedHash('TapTweak', childNodeXOnlyPubkeyPrimary), 59 | ); 60 | 61 | // Do a sanity check with the WIF serialized and then verify childNodePrimary is the same 62 | const wif = childNodePrimary.toWIF(); 63 | const keypair = ECPair.fromWIF(wif); 64 | 65 | if (childNodePrimary.publicKey.toString('hex') !== keypair.publicKey.toString('hex')) { 66 | throw 'createKeyPair error child node not match sanity check' 67 | } 68 | return { 69 | address: p2trPrimary.address, 70 | publicKey: childNodePrimary.publicKey.toString('hex'), 71 | publicKeyXOnly: childNodeXOnlyPubkeyPrimary.toString('hex'), 72 | path, 73 | WIF: childNodePrimary.toWIF(), 74 | privateKey: childNodePrimary.privateKey?.toString('hex'), 75 | } 76 | } 77 | export interface WalletRequestDefinition { 78 | phrase?: string | undefined 79 | path?: string | undefined 80 | } 81 | 82 | export const createPrimaryAndFundingImportedKeyPairs = async ( 83 | phrase?: string | undefined, 84 | path?: string | undefined, 85 | passphrase?: string | undefined, 86 | n?: number 87 | ) => { 88 | if (!phrase) { 89 | phrase = createMnemonicPhrase().phrase; 90 | } 91 | let pathUsed = defaultDerivedPath.substring(0, 11); 92 | if (path) { 93 | pathUsed = path; 94 | } 95 | const imported = {} 96 | 97 | if (n) { 98 | for (let i = 2; i < n + 2; i++) { 99 | imported[i+''] = await createKeyPair(phrase, `${pathUsed}/0/` + i, passphrase) 100 | } 101 | } 102 | return { 103 | wallet: { 104 | phrase, 105 | passphrase, 106 | primary: await createKeyPair(phrase, `${pathUsed}/0/0`, passphrase), 107 | funding: await createKeyPair(phrase, `${pathUsed}/1/0`, passphrase) 108 | }, 109 | imported 110 | } 111 | } 112 | 113 | export const createNKeyPairs = async ( 114 | phrase: string | undefined, 115 | passphrase: string | undefined, 116 | n = 1 117 | ) => { 118 | const keypairs: any = []; 119 | for (let i = 0; i < n; i++) { 120 | keypairs.push(await createKeyPair(phrase, `${defaultDerivedPath.substring(0, 13)}/${i}`, passphrase)); 121 | } 122 | return { 123 | phrase, 124 | keypairs, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/utils/create-mnemonic-phrase.ts: -------------------------------------------------------------------------------- 1 | const bitcoin = require('bitcoinjs-lib'); 2 | import * as ecc from 'tiny-secp256k1'; 3 | import { randomBytes } from 'crypto'; 4 | const bip39 = require('bip39'); 5 | 6 | bitcoin.initEccLib(ecc); 7 | 8 | export function createMnemonicPhrase() : ({ 9 | phrase: string 10 | }) { 11 | const mnemonic = bip39.entropyToMnemonic(randomBytes(16).toString('hex')) 12 | if (!bip39.validateMnemonic(mnemonic)) { 13 | throw new Error("Invalid mnemonic generated!"); 14 | } 15 | return { 16 | phrase: mnemonic 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/utils/decode-mnemonic-phrase.ts: -------------------------------------------------------------------------------- 1 | const bitcoin = require('bitcoinjs-lib'); 2 | import ECPairFactory from 'ecpair'; 3 | import * as ecc from 'tiny-secp256k1'; 4 | bitcoin.initEccLib(ecc); 5 | 6 | const ECPair = ECPairFactory(ecc); 7 | import BIP32Factory from 'bip32'; 8 | import { NETWORK } from '../commands/command-helpers'; 9 | const bip32 = BIP32Factory(ecc); 10 | 11 | const toXOnly = (publicKey) => { 12 | return publicKey.slice(1, 33); 13 | } 14 | const bip39 = require('bip39'); 15 | 16 | export const decodeMnemonicPhrase = async (phrase: string, path: string, passphrase?: string) => { 17 | if (!bip39.validateMnemonic(phrase)) { 18 | throw new Error("Invalid mnemonic phrase provided!"); 19 | } 20 | const seed = await bip39.mnemonicToSeed(phrase, passphrase); 21 | const rootKey = bip32.fromSeed(seed); 22 | const childNode = rootKey.derivePath(path); 23 | // const { address } = bitcoin.payments.p2pkh({ pubkey: childNode.publicKey }); 24 | 25 | const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); 26 | const p2tr = bitcoin.payments.p2tr({ 27 | internalPubkey: childNodeXOnlyPubkey, 28 | network: NETWORK 29 | }); 30 | if (!p2tr.address || !p2tr.output) { 31 | throw "error creating p2tr" 32 | } 33 | // Used for signing, since the output and address are using a tweaked key 34 | // We must tweak the signer in the same way. 35 | const tweakedChildNode = childNode.tweak( 36 | bitcoin.crypto.taggedHash('TapTweak', childNodeXOnlyPubkey), 37 | ); 38 | 39 | return { 40 | phrase, 41 | address: p2tr.address, 42 | publicKey: childNode.publicKey.toString('hex'), 43 | publicKeyXOnly: childNodeXOnlyPubkey.toString('hex'), 44 | path, 45 | WIF: childNode.toWIF(), 46 | privateKey: childNode.privateKey?.toString('hex'), 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /lib/utils/file-utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | export const fileReader = async (filePath, encoding?: any) => { 4 | return new Promise((resolve, reject) => { 5 | fs.readFile(filePath, encoding, (err, fileData: string) => { 6 | if (err) { 7 | console.log(`Error reading ${filePath}`, err); 8 | return reject(err); 9 | } 10 | try { 11 | return resolve(fileData); 12 | } catch (err) { 13 | console.log(`Error reading ${filePath}`, err); 14 | return reject(null); 15 | } 16 | }) 17 | }); 18 | } 19 | 20 | export const jsonFileReader = async (filePath) : Promise => { 21 | return new Promise((resolve, reject) => { 22 | fs.readFile(filePath, (err, fileData: any) => { 23 | if (err) { 24 | console.log(`Error reading ${filePath}`, err); 25 | return reject(err); 26 | } 27 | try { 28 | const object = JSON.parse(fileData) 29 | return resolve(object); 30 | } catch (err) { 31 | console.log(`Error reading ${filePath}`, err); 32 | return reject(null); 33 | } 34 | }) 35 | }); 36 | } 37 | 38 | export const jsonFileWriter = async (filePath, data) => { 39 | return new Promise(function (resolve, reject) { 40 | fs.writeFile(filePath, Buffer.from(JSON.stringify(data,null, 2)), 'utf8', function (err) { 41 | if (err) { 42 | console.log('jsonFileWriter', err); 43 | reject(err); 44 | } 45 | else { 46 | 47 | resolve(true); 48 | } 49 | }); 50 | }) 51 | }; 52 | 53 | /* 54 | 55 | export const jsonFileWriter = async (filePath, data) => { 56 | 57 | return new Promise(function (resolve, reject) { 58 | const stringifyStream = json.createStringifyStream({ 59 | body: data 60 | }); 61 | var fd = fs.openSync(filePath, 'w'); 62 | 63 | fs.closeSync(fs.openSync(filePath, 'w')); 64 | 65 | stringifyStream.on('data', function (strChunk) { 66 | fs.appendFile(filePath, strChunk, function (err) { 67 | if (err) throw err; 68 | }) 69 | }); 70 | stringifyStream.on('end', function () { 71 | resolve(true); 72 | }) 73 | }); 74 | 75 | }; 76 | 77 | const json = require('big-json'); 78 | 79 | // pojo will be sent out in JSON chunks written to the specified file name in the root 80 | function makeFile(filename, pojo){ 81 | 82 | const stringifyStream = json.createStringifyStream({ 83 | body: pojo 84 | }); 85 | 86 | stringifyStream.on('data', function(strChunk) { 87 | fs.appendFile(filename, strChunk, function (err) { 88 | if (err) throw err; 89 | }) 90 | }); 91 | 92 | } 93 | 94 | */ 95 | 96 | export const fileWriter = async (filePath, data) => { 97 | return new Promise(function (resolve, reject) { 98 | fs.writeFile(filePath, data, 'utf8', function (err) { 99 | if (err) { 100 | console.log('fileWriter', err); 101 | reject(err); 102 | } 103 | else { 104 | resolve(true); 105 | } 106 | }); 107 | }) 108 | }; 109 | 110 | export const jsonFileExists = async (filePath) => { 111 | return new Promise(function (resolve, reject) { 112 | fs.exists(filePath, function (exists) { 113 | resolve(exists); 114 | }); 115 | }) 116 | }; 117 | 118 | 119 | export function chunkBuffer(buffer: any, chunkSize: number) { 120 | assert(!isNaN(chunkSize) && chunkSize > 0, 'Chunk size should be positive number'); 121 | const result: any = []; 122 | const len = buffer.byteLength; 123 | let i = 0; 124 | while (i < len) { 125 | result.push(buffer.slice(i, i += chunkSize)); 126 | } 127 | return result; 128 | } 129 | 130 | function assert(cond, err) { 131 | if (!cond) { 132 | throw new Error(err); 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /lib/utils/hydrate-config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationInterface } from '../interfaces/configuration.interface'; 2 | export function hydrateConfig(config: ConfigurationInterface) { 3 | return config; 4 | } 5 | -------------------------------------------------------------------------------- /lib/utils/prompt-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as readline from 'readline'; 2 | 3 | /** 4 | * Warn or continue 5 | * @param msg Msg to display 6 | * @param success Success criteria input match 7 | * @returns 8 | */ 9 | export const warnContinueAbort = async (msg, success) => { 10 | const rl = readline.createInterface({ 11 | input: process.stdin, 12 | output: process.stdout 13 | }); 14 | 15 | try { 16 | let reply: string = ''; 17 | const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); 18 | 19 | reply = (await prompt(msg) as any); 20 | if (reply === success) { 21 | return; 22 | } 23 | throw 'Cancelled'; 24 | } finally { 25 | rl.close(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/utils/select-funding-utxo.ts: -------------------------------------------------------------------------------- 1 | import { type ElectrumApiInterface } from "../api/electrum-api.interface"; 2 | import { type UTXO } from "../types/UTXO.interface"; 3 | import * as bitcoin from 'bitcoinjs-lib'; 4 | import * as ecc from 'tiny-secp256k1'; 5 | import * as qrcode from 'qrcode-terminal'; 6 | bitcoin.initEccLib(ecc); 7 | 8 | export const getInputUtxoFromTxid = async (utxo: UTXO, electrumx: ElectrumApiInterface) => { 9 | const txResult = await electrumx.getTx(utxo.txId); 10 | 11 | if (!txResult || !txResult.success) { 12 | throw `Transaction not found in getInputUtxoFromTxid ${utxo.txId}`; 13 | } 14 | const tx = txResult.tx; 15 | utxo.nonWitnessUtxo = Buffer.from(tx, 'hex'); 16 | 17 | const reconstructedTx = bitcoin.Transaction.fromHex(tx.tx); 18 | if (reconstructedTx.getId() !== utxo.txId) { 19 | throw "getInputUtxoFromTxid txid mismatch error"; 20 | } 21 | 22 | return utxo; 23 | } 24 | 25 | export const getFundingSelectedUtxo = async (address: string, minFundingSatoshis: number, electrumx: ElectrumApiInterface): Promise => { 26 | // Query for a UTXO 27 | const listunspents = await electrumx.getUnspentAddress(address); 28 | const utxos = listunspents.utxos.filter((utxo) => { 29 | return utxo.value >= minFundingSatoshis 30 | }); 31 | if (!utxos.length) { 32 | throw new Error(`Unable to select funding utxo, check at least 1 utxo contains ${minFundingSatoshis} satoshis`); 33 | } 34 | const selectedUtxo = utxos[0]; 35 | return getInputUtxoFromTxid(selectedUtxo, electrumx); 36 | } 37 | 38 | /** 39 | * Gets a funding UTXO and also displays qr code for quick deposit 40 | * @param electrumxApi 41 | * @param address 42 | * @param amount 43 | * @returns 44 | */ 45 | export const getFundingUtxo = async (electrumxApi, address: string, amount: number, suppressDepositAddressInfo = false, seconds = 5) => { 46 | // We are expected to perform commit work, therefore we must fund with an existing UTXO first to generate the commit deposit address 47 | if (!suppressDepositAddressInfo) { 48 | qrcode.generate(address, { small: false }); 49 | } 50 | // If commit POW was requested, then we will use a UTXO from the funding wallet to generate it 51 | console.log(`...`) 52 | console.log(`...`) 53 | if (!suppressDepositAddressInfo) { 54 | console.log(`WAITING UNTIL ${amount / 100000000} BTC RECEIVED AT ${address}`) 55 | } 56 | console.log(`...`) 57 | console.log(`...`) 58 | const fundingUtxo = await electrumxApi.waitUntilUTXO(address, amount, seconds ? 5 : seconds, false); 59 | console.log(`Detected Funding UTXO (${fundingUtxo.txid}:${fundingUtxo.vout}) with value ${fundingUtxo.value} for funding...`); 60 | return fundingUtxo 61 | } 62 | -------------------------------------------------------------------------------- /lib/utils/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export const sleeper = async (seconds: number) => { 3 | return new Promise((resolve) => { 4 | setTimeout(() => { 5 | resolve(true); 6 | }, seconds * 1000); 7 | }) 8 | } 9 | 10 | export function onlyUnique(value: T, index: number, array: T[]) { 11 | return array.indexOf(value) === index; 12 | } 13 | 14 | export function isHex(str) { 15 | const regexp = /^[0-9a-f]+$/; 16 | if (regexp.test(str)) { 17 | return true; 18 | } 19 | else { 20 | return false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/utils/validate-cli-inputs.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationInterface } from '../interfaces/configuration.interface'; 2 | export const validateCliInputs = (): ConfigurationInterface => { 3 | // Validate the BITCOIND_RPCURL 4 | if (process.env.ELECTRUMX_PROXY_BASE_URL) { 5 | return { 6 | electrumxWebsocketUrl: process.env.ELECTRUMX_PROXY_BASE_URL 7 | } 8 | } 9 | 10 | throw new Error('invalid config'); 11 | } 12 | -------------------------------------------------------------------------------- /lib/utils/validate-wallet-storage.ts: -------------------------------------------------------------------------------- 1 | const bitcoin = require('bitcoinjs-lib'); 2 | import ECPairFactory from 'ecpair'; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import { ConfigurationInterface } from '../interfaces/configuration.interface'; 5 | bitcoin.initEccLib(ecc); 6 | const ECPair = ECPairFactory(ecc); 7 | import * as bip39 from 'bip39'; 8 | import BIP32Factory from 'bip32'; 9 | import { jsonFileReader } from './file-utils'; 10 | import { toXOnly } from './create-key-pair'; 11 | import { walletPathResolver } from './wallet-path-resolver'; 12 | import { NETWORK } from '../commands/command-helpers'; 13 | const bip32 = BIP32Factory(ecc); 14 | const walletPath = walletPathResolver(); 15 | 16 | export interface IWalletRecord { 17 | address: string, 18 | WIF: string, 19 | childNode?: any 20 | } 21 | 22 | export interface IValidatedWalletInfo { 23 | primary: IWalletRecord; 24 | funding: IWalletRecord; 25 | imported: { 26 | [alias: string]: IWalletRecord; 27 | } 28 | } 29 | 30 | export const validateWalletStorage = async (): Promise => { 31 | try { 32 | console.log('walletPath', walletPath); 33 | const wallet: any = await jsonFileReader(walletPath); 34 | if (!wallet.phrase) { 35 | console.log(`phrase field not found in ${walletPath}`); 36 | throw new Error(`phrase field not found in ${walletPath}`) 37 | } 38 | 39 | // Validate is a valid mnemonic 40 | if (!bip39.validateMnemonic(wallet.phrase)) { 41 | console.log('phrase is not a valid mnemonic phrase!'); 42 | throw new Error("phrase is not a valid mnemonic phrase!"); 43 | } 44 | 45 | if (!wallet.primary) { 46 | console.log(`Wallet needs a primary address`); 47 | throw new Error(`Wallet needs a primary address`); 48 | } 49 | 50 | if (!wallet.funding) { 51 | console.log(`Wallet needs a funding address`); 52 | throw new Error(`Wallet needs a funding address`); 53 | } 54 | 55 | // Validate WIF 56 | if (!wallet.primary.WIF) { 57 | console.log(`Primary WIF not set`); 58 | throw new Error(`Primary WIF not set`); 59 | } 60 | if (!wallet.funding.WIF) { 61 | console.log(`Funding WIF not set`); 62 | throw new Error(`Funding WIF not set`); 63 | } 64 | 65 | // Validate Addresses 66 | if (!wallet.primary.address) { 67 | console.log(`Primary address not set`); 68 | throw new Error(`Primary address not set`); 69 | } 70 | if (!wallet.funding.address) { 71 | console.log(`Funding address not set`); 72 | throw new Error(`Funding address not set`); 73 | } 74 | 75 | const seed = await bip39.mnemonicToSeed(wallet.phrase, wallet.passphrase); 76 | const rootKey = bip32.fromSeed(seed); 77 | const derivePathPrimary = wallet.primary.path; 78 | 79 | const childNodePrimary = rootKey.derivePath(derivePathPrimary); 80 | 81 | const childNodeXOnlyPubkeyPrimary = toXOnly(childNodePrimary.publicKey); 82 | const p2trPrimary = bitcoin.payments.p2tr({ 83 | internalPubkey: childNodeXOnlyPubkeyPrimary, 84 | network: NETWORK 85 | }); 86 | if (!p2trPrimary.address || !p2trPrimary.output) { 87 | throw "error creating p2tr primary" 88 | } 89 | const derivePathFunding = wallet.funding.path; 90 | const childNodeFunding = rootKey.derivePath(derivePathFunding); 91 | const childNodeXOnlyPubkeyFunding = toXOnly(childNodeFunding.publicKey); 92 | const p2trFunding = bitcoin.payments.p2tr({ 93 | internalPubkey: childNodeXOnlyPubkeyFunding, 94 | network: NETWORK 95 | }); 96 | if (!p2trFunding.address || !p2trFunding.output) { 97 | throw "error creating p2tr funding" 98 | } 99 | //const childNodeFunding = rootKey.derivePath(derivePathFunding); 100 | // const { address } = bitcoin.payments.p2pkh({ pubkey: childNode.publicKey }); 101 | // const wif = childNodePrimary.toWIF(); 102 | const keypairPrimary = ECPair.fromWIF(wallet.primary.WIF); 103 | 104 | if (childNodePrimary.toWIF() !== wallet.primary.WIF) { 105 | throw 'primary wif does not match'; 106 | } 107 | 108 | const p2trPrimaryCheck = bitcoin.payments.p2tr({ 109 | internalPubkey: toXOnly(keypairPrimary.publicKey), 110 | network: NETWORK 111 | }); 112 | 113 | if (p2trPrimaryCheck.address !== p2trPrimary.address && p2trPrimary.address !== wallet.primary.address) { 114 | const m = `primary address is not correct and does not match associated phrase at ${derivePathPrimary}. Found: ` + p2trPrimaryCheck.address; 115 | console.log(m); 116 | throw new Error(m); 117 | } 118 | 119 | const keypairFunding = ECPair.fromWIF(wallet.funding.WIF); 120 | 121 | if (childNodeFunding.toWIF() !== wallet.funding.WIF) { 122 | throw 'funding wif does not match'; 123 | } 124 | 125 | const p2trFundingCheck = bitcoin.payments.p2tr({ 126 | internalPubkey: toXOnly(keypairFunding.publicKey), 127 | network: NETWORK 128 | }); 129 | 130 | if (p2trFundingCheck.address !== p2trFundingCheck.address && p2trFundingCheck.address !== wallet.funding.address) { 131 | const m = `funding address is not correct and does not match associated phrase at ${derivePathFunding}. Found: ` + p2trFundingCheck.address; 132 | console.log(m); 133 | throw new Error(m); 134 | } 135 | 136 | // Now we loop over every imported wallet and validate that they are correct 137 | const imported = {} 138 | for (const prop in wallet.imported) { 139 | if (!wallet.imported.hasOwnProperty(prop)) { 140 | continue; 141 | } 142 | // Get the wif and the address and ensure they match 143 | const importedKeypair = ECPair.fromWIF(wallet.imported[prop].WIF); 144 | // Sanity check 145 | if (importedKeypair.toWIF() !== wallet.imported[prop].WIF) { 146 | throw 'Imported WIF does not match'; 147 | } 148 | 149 | const p2trImported = bitcoin.payments.p2tr({ 150 | internalPubkey: toXOnly(importedKeypair.publicKey), 151 | network: NETWORK 152 | }); 153 | 154 | if (p2trImported.address !== wallet.imported[prop].address) { 155 | throw `Imported address does not match for alias ${prop}. Expected: ` + wallet.imported[prop].address + ', Found: ' + p2trImported.address; 156 | } 157 | imported[prop] = { 158 | address: p2trImported.address, 159 | WIF: wallet.imported[prop].WIF 160 | } 161 | } 162 | 163 | return { 164 | primary: { 165 | childNode: childNodePrimary, 166 | address: p2trPrimary.address, 167 | WIF: childNodePrimary.toWIF() 168 | }, 169 | funding: { 170 | childNode: childNodeFunding, 171 | address: p2trFunding.address, 172 | WIF: childNodeFunding.toWIF() 173 | }, 174 | imported 175 | }; 176 | 177 | } catch (err) { 178 | console.log(`Error reading ${walletPath}. Create a new wallet with "npm cli wallet-init"`) 179 | throw err; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /lib/utils/wallet-path-resolver.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | dotenv.config(); 3 | 4 | export const walletPathResolver = (): string => { 5 | let basePath = '.'; 6 | if (process.env.WALLET_PATH) { 7 | basePath = process.env.WALLET_PATH; 8 | } 9 | let fileName = 'wallet.json'; 10 | if (process.env.WALLET_FILE) { 11 | fileName = process.env.WALLET_FILE; 12 | } 13 | const fullPath = `${basePath}/${fileName}`; 14 | return fullPath; 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atomicals-js", 3 | "version": "0.1.81", 4 | "description": "Atomicals Javascript Library and CLI", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "typings": "dist/index.d.ts", 8 | "type": "commonjs", 9 | "scripts": { 10 | "build": "tsc && gulp build", 11 | "cli": "node dist/cli.js", 12 | "test": "mocha --timeout=30000 --reporter spec \"test/**/*.test.js\"", 13 | "e2e": "mocha --grep e2e --timeout=300000 --reporter spec", 14 | "postbuild": "echo '#!/usr/bin/env node\n' | cat - dist/cli.js > temp && mv temp dist/cli.js", 15 | "postinstall": "patch-package" 16 | }, 17 | "bin": { 18 | "atomicals": "./dist/cli.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/atomicals/atomicals-js.git" 23 | }, 24 | "author": "", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/atomicals/atomicals-js/issues" 28 | }, 29 | "keywords": [ 30 | "bitcoin", 31 | "atomicals" 32 | ], 33 | "homepage": "https://github.com/atomicals/atomicals-js", 34 | "devDependencies": { 35 | "chai": "^4.3.4", 36 | "chai-as-promised": "^7.1.1", 37 | "gulp": "^4.0.2", 38 | "gulp-autoprefixer": "^8.0.0", 39 | "gulp-changed": "^4.0.3", 40 | "gulp-clean": "^0.4.0", 41 | "gulp-concat": "^2.6.1", 42 | "gulp-cssnano": "^2.1.3", 43 | "gulp-header": "^2.0.9", 44 | "gulp-imagemin": "^8.0.0", 45 | "gulp-rename": "^2.0.0", 46 | "gulp-sass": "^5.0.0", 47 | "gulp-typescript": "^6.0.0-alpha.1", 48 | "gulp-uglify": "^3.0.2", 49 | "mocha": "^9.1.3", 50 | "patch-package": "^8.0.0", 51 | "typescript": "^4.5.4", 52 | "vinyl-source-stream": "^2.0.0" 53 | }, 54 | "dependencies": { 55 | "JSONStream": "^1.3.5", 56 | "axios": "^1.5.1", 57 | "base32": "^0.0.7", 58 | "base58check": "^2.0.0", 59 | "bip32": "^3.1.0", 60 | "bip39": "^3.0.4", 61 | "bitcoinjs-lib": "^6.1.0", 62 | "borc": "^3.0.0", 63 | "bs58check": "^3.0.1", 64 | "chalk": "4.1.2", 65 | "coinselect": "^3.1.13", 66 | "commander": "^10.0.0", 67 | "crockford-base32": "^2.0.0", 68 | "crypto-js": "^4.2.0", 69 | "dotenv": "^16.0.1", 70 | "ecpair": "^2.1.0", 71 | "electrum-client": "^0.0.6", 72 | "fast-sha256": "^1.3.0", 73 | "js-sha256": "^0.9.0", 74 | "json-stream-stringify": "^3.1.0", 75 | "lodash.clonedeep": "^4.5.0", 76 | "merkletreejs": "^0.3.11", 77 | "mime-types": "^2.1.35", 78 | "path": "^0.12.7", 79 | "qrcode-terminal": "^0.12.0", 80 | "rpc-websockets": "^7.5.0", 81 | "success-motivational-quotes": "^1.0.8", 82 | "terminal-image": "^2.0.0", 83 | "tiny-secp256k1": "^2.2.1", 84 | "varuint-bitcoin": "^1.1.2" 85 | }, 86 | "files": [ 87 | "dist/*" 88 | ], 89 | "directories": { 90 | "lib": "lib", 91 | "test": "test" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /patches/bitcoinjs-lib+6.1.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/bitcoinjs-lib/src/psbt.js b/node_modules/bitcoinjs-lib/src/psbt.js 2 | index 71c3589..223b21e 100644 3 | --- a/node_modules/bitcoinjs-lib/src/psbt.js 4 | +++ b/node_modules/bitcoinjs-lib/src/psbt.js 5 | @@ -249,12 +249,12 @@ class Psbt { 6 | return this; 7 | } 8 | extractTransaction(disableFeeCheck) { 9 | - if (!this.data.inputs.every(isFinalized)) throw new Error('Not finalized'); 10 | + // if (!this.data.inputs.every(isFinalized)) throw new Error('Not finalized'); 11 | const c = this.__CACHE; 12 | if (!disableFeeCheck) { 13 | checkFees(this, c, this.opts); 14 | } 15 | - if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; 16 | + // if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; 17 | const tx = c.__TX.clone(); 18 | inputFinalizeGetAmts(this.data.inputs, tx, c, true); 19 | return tx; 20 | -------------------------------------------------------------------------------- /templates/containers/dmint-collection-general-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Collection Name", 3 | "desc": "Collection description", 4 | "image": "atom:btc:dat:/image.png", 5 | "legal": { 6 | "terms": "...", 7 | "license": "CC" 8 | }, 9 | "links": { 10 | "x": { 11 | "v": "https://x.com/..." 12 | }, 13 | "website": { 14 | "v": "https://..." 15 | }, 16 | "discord": { 17 | "v": "https://discord.gg/..." 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /templates/containers/dmint-sample-complex.json: -------------------------------------------------------------------------------- 1 | { 2 | "note": "this file should not be generated manually but used with prepare-dmint command", 3 | "dmint": { 4 | "v": 1, 5 | "mint_height": 8182000, 6 | "items": 10000, 7 | "merkle": "", 8 | "immutable": true, 9 | "rules": [ 10 | { 11 | "p": "^0.*$", 12 | "o": { 13 | "5120c2521c3abacd75346eb3115ca1270225ba2bab5d273b7c8b1eab34b21deea861": { 14 | "v": 1000 15 | }, 16 | "5120c2529c35bacd75346eb3115ca1270325ba2bab5d573b7c8a1eab54b21d4ea86e": { 17 | "v": 600, 18 | "id": "2112003789d572f764f391b2732663997e53a1153e01dc8a4fca020eaff324501i0" 19 | } 20 | }, 21 | "bitworkc": "4180" 22 | }, 23 | { 24 | "p": "^.*\\.gif$", 25 | "bitworkc": "888888" 26 | }, 27 | { 28 | "p": ".*", 29 | "bitworkc": "1234", 30 | "bitworkr": "333" 31 | } 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /templates/containers/dmint-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "dmint": { 3 | "v": "1", 4 | "mint_height": 1, 5 | "items": 40, 6 | "rules": [ 7 | { 8 | "p": ".*", 9 | "bitworkc": "1234" 10 | } 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /templates/containers/full-collection-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "realm": { 4 | "v": "atom:btc:realm:myrealmname.subrealm" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /templates/containers/minimum-collection-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Collection Name", 3 | "image": "atom:btc:dat:/image.png", 4 | "items": { 5 | "0": { 6 | "id": "84718b469c40b1bcc74b324b8e24e44442f88cd913687ea2bc7b3e79d4fc4fdei0" 7 | }, 8 | "1": { 9 | "id": "1070d7c98304bf5ac9a5addceb13e0ce4e840641f82d71d84cebbdac42731f4fi0" 10 | }, 11 | "9999": { 12 | "id": "14a0d7c98345bf5ac9a5addceb1de0ce4e840641f82d71d84cebbdac427c53fc3i0" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /templates/data/delete1.json: -------------------------------------------------------------------------------- 1 | { 2 | "some": true, 3 | "dec": "should not update because not true", 4 | "shouldnotadd": 1, 5 | "shouldalsonotadd": 0, 6 | "nested": { 7 | "newfield": "newvaluetest", 8 | "another": true, 9 | "array": true 10 | } 11 | } -------------------------------------------------------------------------------- /templates/data/delete2.json: -------------------------------------------------------------------------------- 1 | { 2 | "some": 7777, 3 | "dec": true, 4 | "nested": true, 5 | "bitcoin.png": true 6 | } -------------------------------------------------------------------------------- /templates/data/delete3.json: -------------------------------------------------------------------------------- 1 | { 2 | "some": true, 3 | "dec": true, 4 | "nested": true, 5 | "bitcoin.png": true, 6 | "shouldnotadd": true, 7 | "shouldalsonotadd": true, 8 | "forrealnotaddbetternot": false, 9 | "forrealalsonotadd": 1, 10 | "hello": true 11 | } 12 | 13 | -------------------------------------------------------------------------------- /templates/data/update1.json: -------------------------------------------------------------------------------- 1 | { 2 | "some": "value", 3 | "dec": 9183.32310, 4 | "nested": { 5 | "struct": "data.", 6 | "another": { 7 | "inside": "omega", 8 | "number": 123 9 | }, 10 | "array": [ 11 | { 12 | "elem": 12, 13 | "elem2": "sss" 14 | }, 15 | { 16 | "item": 99, 17 | "item2": "xyz" 18 | } 19 | ] 20 | }, 21 | "bitcoin.png": { 22 | "replaced": "value" 23 | } 24 | } -------------------------------------------------------------------------------- /templates/data/update2.json: -------------------------------------------------------------------------------- 1 | { 2 | "some": 7777, 3 | "dec": 1679, 4 | "nested": { 5 | "newfield": "newvalue", 6 | "another": { 7 | "inside": "betaupdated", 8 | "number": 42 9 | }, 10 | "array": { 11 | "sub": "elementupdated" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /templates/fungible-tokens/DISCLAIMER.md: -------------------------------------------------------------------------------- 1 | # DISCLAIMER AND LEGAL NOTICE 2 | 3 | You must consult legal advice and ensure you are in legal compliance in all the jurisdiction that apply to you and the users of your tokens. 4 | 5 | Warning: The terms herein are only an example and not meant as legal advice nor a recommendation. You are advised to consult legal advice. -------------------------------------------------------------------------------- /templates/fungible-tokens/full-ft-meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TOKEN", 3 | "desc": "", 4 | "image": "atom:btc:dat:/image.png", 5 | "links": { 6 | "x": { 7 | "v": "https://x.com/yourtwttiernameifyouhaveone" 8 | }, 9 | "website": { 10 | "v": "https://your-website-url-ifyouhaveone.com" 11 | }, 12 | "realm": { 13 | "v": "atom:btc:realm:myrealmname.subrealm" 14 | } 15 | }, 16 | "legal": { 17 | "terms": "" 18 | } 19 | } -------------------------------------------------------------------------------- /templates/fungible-tokens/minimum-ft-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "desc": "", 4 | "image": "atom:btc:dat:/image.png", 5 | "legal": { 6 | "terms": "" 7 | } 8 | } -------------------------------------------------------------------------------- /templates/fungible-tokens/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "desc": "", 4 | "image": "atom:btc:dat:/image.png", 5 | "legal": { 6 | "terms": "The Token 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 non-infringement. In no event shall the creators, 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 Token or the use or other dealings in The Token. The Token does not represent any investment, security, financial instrument, redemption, promise, bearer instrument or commitment of any kind. The Token is intended only for educational and experimentation purposes only and is not backed or supported by any individual or team. There are no future prospects or plans of any kind beyond the educational and experimentation usages of The Token. Any use or interaction with The Token is expressly prohibited unless your jurisdiction and circumstances explicitly permits the use and interaction with The Token. Any interaction with The Token constitutes acceptance of these terms and the user accepts all responsibility and all risks associated with the use and interaction with The Token." 7 | } 8 | } -------------------------------------------------------------------------------- /templates/fungible-tokens/terms.md: -------------------------------------------------------------------------------- 1 | The Token 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 non-infringement. In no event shall the creators, 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 Token or the use or other dealings in The Token. The Token does not represent any investment, security, financial instrument, redemption, promise, bearer instrument or commitment of any kind. The Token is intended only for educational and experimentation purposes only and is not backed or supported by any individual or team. There are no future prospects or plans of any kind beyond the educational and experimentation usages of The Token. Any use or interaction with The Token is expressly prohibited unless your jurisdiction and circumstances explicitly permits the use and interaction with The Token. Any interaction with The Token constitutes acceptance of these terms and the user accepts all responsibility and all risks associated with the use and interaction with The Token. -------------------------------------------------------------------------------- /templates/socialfi/base-profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "v": "1.2", 3 | "name": "User or profile title or name", 4 | "image": "atom:btc:id:/image.jpg", 5 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 6 | "ids": { 7 | "0": { 8 | "t": "realm", 9 | "v": "myrealmname" 10 | }, 11 | "1": { 12 | "t": "realm", 13 | "v": "messenger.cool" 14 | } 15 | }, 16 | "wallets": { 17 | "btc": { 18 | "address": "btc address to receive tips" 19 | }, 20 | "doge": { 21 | "address": "doge address to receive tips" 22 | }, 23 | "nostr": { 24 | "nip05": "...any other nostr fields" 25 | } 26 | }, 27 | "links": { 28 | "0": { 29 | "group": "social", 30 | "items": { 31 | "0": { 32 | "type": "instagram", 33 | "name": "@loremipsum", 34 | "url": "https://instagram.com/loremipsum9872" 35 | }, 36 | "1": { 37 | "type": "x", 38 | "name": "@loremipsum9872", 39 | "url": "https://x.com/loremipsum9872" 40 | }, 41 | "2": { 42 | "type": "youtube", 43 | "name": "@loremipsum9872", 44 | "url": "https://www.youtube.com/@GuitarMelodies2023" 45 | } 46 | } 47 | } 48 | }, 49 | "collections": { 50 | "container-name1": { 51 | "name": "my cool collection", 52 | "image": "atom:btc:id:/image.jpg optional but recommended image", 53 | "desc": "Optional collection info", 54 | "meta": { 55 | "order": 1, 56 | "note": "This section can be used to give presentation instructions", 57 | "display": "gallery" 58 | }, 59 | "preview": { 60 | "0": { 61 | "type": "previewbox", 62 | "text": "caption text hint", 63 | "name": "@loremipsum", 64 | "url": "https://instagram.com/loremipsum9872", 65 | "img": "optional image url", 66 | "class": [ 67 | "border-none", 68 | "blue-highlight" 69 | ], 70 | "style": { 71 | "border": "none" 72 | } 73 | }, 74 | "1": { 75 | "type": "previewbox", 76 | "text": "caption text hint", 77 | "name": "@loremipsum", 78 | "url": "https://instagram.com/loremipsum9872", 79 | "img": "optional image url", 80 | "class": [ 81 | "border-none", 82 | "blue-highlight" 83 | ], 84 | "style": { 85 | "border": "none" 86 | } 87 | }, 88 | "2": { 89 | "type": "previewbox", 90 | "text": "caption text hint", 91 | "name": "@loremipsum", 92 | "url": "https://instagram.com/loremipsum9872", 93 | "img": "optional image url", 94 | "class": [ 95 | "border-none", 96 | "blue-highlight" 97 | ], 98 | "style": { 99 | "border": "none" 100 | } 101 | } 102 | }, 103 | "links": [ 104 | { 105 | "group": "social", 106 | "meta": { 107 | "order": 1, 108 | "note": "This section can be used to give presentation instructions", 109 | "display": "bottom" 110 | }, 111 | "items": { 112 | "0": { 113 | "type": "instagram", 114 | "name": "@loremipsum", 115 | "url": "https://instagram.com/loremipsum9872" 116 | }, 117 | "1": { 118 | "type": "x", 119 | "name": "@loremipsum9872", 120 | "url": "https://x.com/loremipsum9872" 121 | }, 122 | "2": { 123 | "type": "youtube", 124 | "name": "@loremipsum9872", 125 | "url": "https://www.youtube.com/@GuitarMelodies2023" 126 | } 127 | } 128 | } 129 | ] 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /templates/socialfi/min-profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "v": "1.2", 3 | "name": "User or profile title or name", 4 | "image": "atom:btc:id:/image.jpg", 5 | "desc": "Lorem ipsum dolor sit amet...", 6 | "ids": { 7 | "0": { 8 | "t": "realm", 9 | "v": "myrealmname" 10 | } 11 | }, 12 | "wallets": { 13 | "btc": { 14 | "address": "btc address to receive tips" 15 | } 16 | }, 17 | "links": { 18 | "0": { 19 | "group": "social", 20 | "items": { 21 | "0": { 22 | "type": "x", 23 | "name": "@loremipsum9872", 24 | "url": "https://x.com/loremipsum9872" 25 | } 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /templates/socialfi/readme.md: -------------------------------------------------------------------------------- 1 | # Social FI Templates for Atomicals 2 | 3 | ## Purpose and use cases 4 | 5 | Host a social profile, brand or website at a (sub)realm like +myusername/profile 6 | 7 | ## Recommended Delegation 8 | 9 | A realm should not contain the profile object directly as data because that would pollute the history of the realm over time. 10 | Instead, use a delegation pattern to link to another Atomical NFT that contains the profile information. 11 | 12 | The advantage is that if a realm changes hands, then the new owner can quickly update the realm to point to their own Atomical NFT 13 | profile object immediately that they may have created earlier or migrated from other realms. **Note: We actually recommend always using the `"d"` delegation 14 | pattern even for any kind of realm zone record or profile data. The clients and services can easily resolve the type of profile with the `v` version field 15 | in the delegate to know how to handle the data in the delegate. This will ensure the Realm histories are unpolluted and kept as simple as possible. 16 | 17 | Example: 18 | 19 | In the realm data store just a delegation like this: 20 | 21 | ``` 22 | { 23 | "d": "" 24 | } 25 | ``` 26 | The convention to use "d" for delegation is intended to be as concise as possible and indicate to services and sites to follow through to the "d" atomicalid 27 | 28 | Then in the NFT have the base-profile.json information. 29 | 30 | ## base-profile.json 31 | 32 | The `base-profile.json` is a starting recommendation for hosting a social profile as an Atomicals profile for users and brands. 33 | 34 | ## Recommended minimum fields 35 | 36 | The minimum fields recommended are to use: 37 | 38 | - v (Fixed version at "v1") 39 | - name 40 | - image (urn to on-chain image stored with `dat` data storage) 41 | - desc 42 | 43 | ## Additional notes 44 | 45 | The structure of the profile is designed in such a way that a partial update of the profile and links can be done with just the updated section. 46 | We opted for using a map instead of an array for the links for that reason since arrays must be completely replaced, whereas objects can have individual 47 | fields replaced. 48 | 49 | ## Version History 50 | 51 | ### 1.2 52 | 53 | Changes: 54 | 55 | - Added optional 'ids' section to be used to link to Realms or other resources to avoid spoofing 56 | 57 | Example of "ids" section of associating a realm `+myrealmname` and another subrealm `+messenger.cool`. 58 | Note that even though it's a subrealm, the type `realm` can be used 59 | 60 | ```json 61 | // Note "t" stands for "type" 62 | // Note "v" stands for "value" 63 | "ids": { 64 | "0": { 65 | "t": "realm", 66 | "v": "myrealmname" 67 | }, 68 | "1": { 69 | "t": "realm", 70 | "v": "messenger.cool" 71 | } 72 | } 73 | ``` 74 | ### 1.1 75 | 76 | - Added optional 'collections' section for NFT creators to showcase the collections they created 77 | - Improve some link formatting 78 | 79 | ### 1 (First version) 80 | 81 | - Establish base socialfi profile to be used with Realm +names 82 | - Supports "link tree" link profiles with avatar, bio, name, links, crypto addresses, etc -------------------------------------------------------------------------------- /templates/subrealms/sample-rules-arc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "subrealms": { 4 | "rules": [ 5 | { 6 | "p": "a.*", 7 | "o": { 8 | "5120c2529c3445d75346eb3115ca1270325ba2bab5d573b7c8a1eab54b21deea86e": { 9 | "v": 1000, 10 | "id": "11120037892572f764ff91b273266937e3a5153e017dc8a4fca020eaff824301i0" 11 | } 12 | } 13 | }, 14 | { 15 | "p": "b.*", 16 | "o": { 17 | "5120c2529c34bacd75346e23115ca1270365ba2bab5d47377c8b1eab54b21deea361": { 18 | "v": 546 19 | }, 20 | "5120c2529c25bacd73346eb3115ca1270325ba2bab55473b7c8a1eab54b21d5ea86e": { 21 | "v": 1001, 22 | "id": "1112003749d572f764ff91b273266997e3a5453e017dc8a4fca020ea5f824301i0" 23 | } 24 | } 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /templates/subrealms/sample-rules.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "subrealms": { 4 | "rules": [ 5 | { 6 | "p": ".*", 7 | "o": { 8 | "512012529135b4cd75646eb3115ca1240325ba2babad435b7c8a1eab55b21dee586e": { 9 | "v": 546 10 | } 11 | } 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/commands/wallet-create.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | var expect = require('chai').expect; 4 | var chaiAsPromised = require('chai-as-promised'); 5 | chai.use(chaiAsPromised); 6 | var index = require('../../dist/index.js'); 7 | 8 | describe('wallet-create', () => { 9 | it('success', async () => { 10 | const result = await index.Atomicals.walletCreate(); 11 | expect(result.success).to.be.true; 12 | expect(result.data.wallet.phrase).to.not.equal(undefined) 13 | expect(result.data.wallet.primary.WIF).to.not.equal(undefined) 14 | expect(result.data.wallet.primary.address).to.not.equal(undefined) 15 | expect(result.data.wallet.primary.privateKey).to.not.equal(undefined) 16 | expect(result.data.wallet.primary.publicKey).to.not.equal(undefined) 17 | expect(result.data.wallet.primary.publicKeyXOnly).to.not.equal(undefined) 18 | expect(result.data.wallet.primary.path).to.not.equal(undefined) 19 | expect(result.data.wallet.funding.WIF).to.not.equal(undefined) 20 | expect(result.data.wallet.funding.address).to.not.equal(undefined) 21 | expect(result.data.wallet.funding.privateKey).to.not.equal(undefined) 22 | expect(result.data.wallet.funding.publicKey).to.not.equal(undefined) 23 | expect(result.data.wallet.funding.publicKeyXOnly).to.not.equal(undefined) 24 | expect(result.data.wallet.funding.path).to.not.equal(undefined) 25 | 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 60000 2 | 3 | -------------------------------------------------------------------------------- /test/utils/atomical-format-helpers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | var expect = require('chai').expect; 4 | var chaiAsPromised = require('chai-as-promised'); 5 | chai.use(chaiAsPromised); 6 | var index = require('../../dist/index.js'); 7 | 8 | describe('atomical-format-helpers test', () => { 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /test/utils/atomical_decorator.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | var expect = require('chai').expect; 4 | var chaiAsPromised = require('chai-as-promised'); 5 | chai.use(chaiAsPromised); 6 | var index = require('../../dist/index.js'); 7 | 8 | describe('atomical_decorator test', () => { 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /test/utils/create-key-pair.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | var expect = require('chai').expect; 4 | var chaiAsPromised = require('chai-as-promised'); 5 | chai.use(chaiAsPromised); 6 | var index = require('../../dist/index.js'); 7 | 8 | describe('create-key-pair', () => { 9 | it('success', async () => { 10 | const result = await index.createKeyPair(); 11 | expect(result.publicKey.length).to.equal(66); 12 | expect(result.privateKey.length).to.equal(64); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/utils/create-mnemonic-phrase.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | var expect = require('chai').expect; 4 | var chaiAsPromised = require('chai-as-promised'); 5 | chai.use(chaiAsPromised); 6 | var index = require('../../dist/index.js'); 7 | 8 | describe('create-mnemonic-phrase', () => { 9 | it('success', async () => { 10 | const result = index.createMnemonicPhrase(); 11 | expect(result.phrase).to.not.be.null 12 | }); 13 | }); -------------------------------------------------------------------------------- /test/utils/script_helpers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | var expect = require('chai').expect; 4 | var chaiAsPromised = require('chai-as-promised'); 5 | chai.use(chaiAsPromised); 6 | var index = require('../../dist/index.js'); 7 | 8 | describe('script_helpers test', () => { 9 | it('success decode p2tr', async () => { 10 | const result = index.detectScriptToAddressType('512009034ea14147937a1fa23c8afe754170ce9ea34571aadd27e29e982d94f06b12'); 11 | expect(result).to.eql("bc1ppyp5ag2pg7fh58az8j90ua2pwr8fag69wx4d6flzn6vzm98sdvfqg0ts4r") 12 | }); 13 | it('success decode witness_v0_keyhash', async () => { 14 | const result = index.detectScriptToAddressType('0014e2cd6ed2fe3567379170d6d9ebd54c63a629f41c'); 15 | expect(result).to.eql("bc1qutxka5h7x4nn0yts6mv7h42vvwnznaquky6v33") 16 | }); 17 | it('success decode p2pkh', async () => { 18 | const result = index.detectScriptToAddressType('76a91459efb00b08d01fb31defbaa77a02be63b1c7eab788ac'); 19 | expect(result).to.eql("19CYGs7iizwJqk8z6UemofDZztYB9YVuW9") 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "resolveJsonModule" : true 10 | } 11 | } 12 | 13 | --------------------------------------------------------------------------------