├── .gitignore ├── .prettierrc ├── README.md ├── code.png ├── package-lock.json ├── package.json ├── src ├── index.ts ├── meta-node.ts ├── node-interface.ts ├── node-interfaces │ ├── bitindex.ts │ └── mattercloud.ts ├── planter.ts ├── untyped.d.ts └── utils.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # planter 2 | 3 | > Create Metanet Nodes on Bitcoin SV 4 | 5 | _planter_ is a simple library for fetching and creating Metanet nodes on the **Bitcoin SV blockchain**. 6 | 7 | ![code](code.png) 8 | 9 | # Setup 10 | 11 | ```bash 12 | npm i planter 13 | ``` 14 | 15 | Include _planter_ in your project 16 | 17 | ```js 18 | import { Planter } from "planter"; 19 | ``` 20 | 21 | ```html 22 | 23 | 24 | ``` 25 | 26 | Be sure to include the [bsv library](https://docs.moneybutton.com/docs/bsv-overview.html) as well when using the web version. 27 | 28 | # Usage 29 | 30 | ```js 31 | const planter = new Planter(); 32 | ``` 33 | 34 | This will generate a wallet for you which will be used to derive node addresses and sign transactions. 35 | You can use an existing wallet by passing an [extended private Key](https://docs.moneybutton.com/docs/bsv-hd-private-key.html) inside of the config object. 36 | 37 | ```js 38 | const planter = new Planter({ 39 | xprivKey: 40 | "xprv9s21ZrQH143K3eQCpBqZiuLgNSFPAfkqimfqyDxJ6HAaVUqWWJ4vz7eZdhgkR66jD1a2BtQEXbYjjbfVXWhxz7g4sNujBt6cnAoJrdfLkHh" 41 | }); 42 | ``` 43 | 44 | Accepted config options are 45 | 46 | - `xprivKey: string` - An extended private Key from which the wallet and nodes are generated. 47 | - `feeb: number` - Fee per byte. Default is `1.4` 48 | - `nodeInterface` - The instance of an implementation of a `NodeInterface`. Implementations exist in `src/node-interfaces/`. 49 | 50 | By default, the Bitindex API is used for pushing transactions and fetching UTXOs. An Implementation for the newer MatterCloud API exists as well. 51 | New MatterCloud instances can be passed an API key, alternatively a new one is automatically requested. 52 | 53 | ```js 54 | import { Mattercloud } from "planter/lib/node-interfaces/mattercloud"; 55 | 56 | const planter = new Planter({ 57 | nodeInterface: new Mattercloud({ 58 | apiKey: "198t2pusaKhaqHSRsfQBtfyE2XR8Xe7Wsd" 59 | }) 60 | }); 61 | ``` 62 | 63 | Funding can be provided by depositing BSV to the associated address. 64 | 65 | ```js 66 | planter.fundingAddress; 67 | ``` 68 | 69 | ## Creating nodes 70 | 71 | ```js 72 | await planter.createNode(options); 73 | ``` 74 | 75 | These additional options can be passed: 76 | 77 | - `data: string[]` - Array of data to include in `OP_RETURN` 78 | - `parentTxID: string` - For creating child nodes. 79 | - `parentKeyPath: string` - Can be passed when `parentTxID` is also passed to override `keyPath` of parent node. 80 | - `keyPath: string` - For setting the keypath manually. Default is `m/0`. 81 | - `safe: boolean` - Use OP_FALSE for scripts. Default is `true`. 82 | - `includeKeyPath: boolean` - Write `keyPath` information to `OP_RETURN`. Defaults to `true`. Can be deactivated to manage keyPaths locally. 83 | 84 | Successfully creating nodes returns an object that contains the new nodes `address`, `id`, `txid` and used `keyPath`. 85 | 86 | ## Traversing the Metanet 87 | 88 | ```js 89 | await planter.findAllNodes(); 90 | ``` 91 | 92 | This will query all nodes owned by the `Planter` instance. 93 | 94 | _planter_ is built on top of _[TreeHugger](https://treehugger.bitpaste.app/)_ and exposes its API for querying and traversing metanet nodes. See TreeHuggers [Github page](https://github.com/libitx/tree-hugger) for details. 95 | 96 | _planter_ also exposes TreeHugger directly for general node querying. 97 | 98 | ```js 99 | import { TreeHugger } from "planter"; 100 | 101 | const node = await TreeHugger.findNodeByTxid(txid); 102 | ``` 103 | 104 | ### Queries 105 | 106 | ```js 107 | await planter.findSingleNode(query); 108 | await planter.findAllNodes(query); 109 | 110 | await planter.findNodeById(id); 111 | await planter.findNodeByTxid(txid); 112 | await planter.findNodesByAddress(address); 113 | await planter.findNodesByParentId(id); 114 | await planter.findNodeAndDescendants(id); 115 | ``` 116 | 117 | ### Relative traversal 118 | 119 | ```js 120 | await node.root(); 121 | await node.parent(); 122 | await node.ancestors(); 123 | await node.siblings(); 124 | await node.children(); 125 | await node.descendants(); 126 | await node.selfAndAncestors(); 127 | await node.selfAndSiblings(); 128 | await node.selfAndChildren(); 129 | await node.selfAndDescendants(); 130 | ``` 131 | 132 | ## Creating child nodes and updates 133 | 134 | ```js 135 | await node.createChild(planter, options); 136 | await node.createUpdate(planter, options); 137 | ``` 138 | 139 | The same options as before are accepted. Additionally, the `Planter` instance that should be used has to be passed. 140 | 141 | ## Node properties 142 | 143 | ```js 144 | node.keyPath; // extracts keyPath out of OP_RETURN if it exists. Rerturns undefined otherwise 145 | 146 | // Properties inherited from Treehugger 147 | node.id; // Metanet node id 148 | node.txid; // Transaction id 149 | node.address; // Metanet node address 150 | 151 | node.isRoot; 152 | node.isChild; 153 | node.isLeaf; 154 | 155 | node.tx; // Planaria tx object 156 | 157 | node.inputs; // Shortcut to node.tx.in 158 | node.outputs; // Shortcut to node.tx.out 159 | node.opReturn; // Shortcut to the OP_RETURN output object 160 | ``` 161 | 162 | ## Under the hood 163 | 164 | _planter_ randomly generates the keypaths used to derive node addresses to avoid accidental reuse and writes them onto the `OP_RETURN` data right after the metanet protocol keywords. 165 | -------------------------------------------------------------------------------- /code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlinB/planter/f8494754c9a501d4d4aa9edcf9441d7208a1c68a/code.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "planter", 3 | "version": "0.4.0", 4 | "description": "Create MetaNet Nodes on Bitcoin SV", 5 | "author": "Merlin Buczek", 6 | "main": "lib/index.js", 7 | "unpkg": "dist/planter.min.js", 8 | "browser": "dist/planter.min.js", 9 | "files": [ 10 | "lib", 11 | "dist" 12 | ], 13 | "license": "MIT", 14 | "repository": "https://github.com/MerlinB/planter", 15 | "keywords": [ 16 | "bsv", 17 | "bitcoinsv", 18 | "bitcoin", 19 | "metanet" 20 | ], 21 | "scripts": { 22 | "build": "tsc && webpack", 23 | "dev": "tsc && webpack --mode=development --devtool=inline-source-map", 24 | "lint": "tslint -p tsconfig.json", 25 | "prepare": "npm run build" 26 | }, 27 | "dependencies": { 28 | "axios": "^0.19.2", 29 | "bsv": "^1.2.0", 30 | "mattercloudjs": "^1.0.6", 31 | "bitindex-sdk": "^3.4.2", 32 | "meta-tree-hugger": "^0.1.0", 33 | "webpack-bundle-analyzer": "^3.5.2" 34 | }, 35 | "devDependencies": { 36 | "ts-loader": "^6.2.0", 37 | "tslint": "^5.20.0", 38 | "tslint-config-prettier": "^1.18.0", 39 | "typescript": "^3.6.3", 40 | "webpack": "^4.41.0", 41 | "webpack-cli": "^3.3.9" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import TreeHugger from "meta-tree-hugger"; 2 | import MetaNode from "./meta-node"; 3 | import { Planter } from "./planter"; 4 | 5 | TreeHugger.db.mapObject = obj => new MetaNode(obj); 6 | 7 | export { TreeHugger }; 8 | export { Planter }; 9 | -------------------------------------------------------------------------------- /src/meta-node.ts: -------------------------------------------------------------------------------- 1 | import bsv from "bsv"; 2 | import THNode from "meta-tree-hugger/lib/meta-node"; 3 | import { INodeOptions, Planter } from "./planter"; 4 | 5 | export default class MetaNode extends THNode { 6 | constructor(tx) { 7 | super(tx); 8 | } 9 | 10 | get keyPath(): string { 11 | const metaOutput = this.opReturn.find(c => c.cell.length && c.cell[0].s === "meta"); 12 | const pathString = metaOutput && metaOutput.cell.length >= 4 ? metaOutput.cell[3].s : null; 13 | 14 | if (bsv.HDPrivateKey.isValidPath(pathString)) { 15 | return pathString; 16 | } 17 | } 18 | 19 | public async createChild(wallet: Planter, { parentTxID, parentKeyPath, ...opts }: INodeOptions = {}) { 20 | if (parentTxID) { 21 | throw new Error("parentTxID cannot be overriden when creating a child node"); 22 | } 23 | 24 | if (this.keyPath && parentKeyPath) { 25 | throw new Error("parent node keyPath already set in OP_RETURN"); 26 | } else if (!this.keyPath && !parentKeyPath) { 27 | throw new Error("No keyPath provided for parent node"); 28 | } 29 | 30 | parentKeyPath = parentKeyPath || this.keyPath; 31 | 32 | return await wallet.createNode({ 33 | ...opts, 34 | parentKeyPath, 35 | parentTxID: this.txid 36 | }); 37 | } 38 | 39 | public async createUpdate(wallet: Planter, { keyPath, parentTxID, ...opts }: INodeOptions = {}) { 40 | if (this.keyPath && keyPath) { 41 | throw new Error("keyPath already set in OP_RETURN"); 42 | } else if (!this.keyPath && !keyPath) { 43 | throw new Error("No keyPath provided for existing node"); 44 | } 45 | 46 | keyPath = keyPath || this.keyPath; 47 | 48 | if (!parentTxID) { 49 | parentTxID = this.isRoot ? null : this.tx.parent.tx; 50 | } 51 | 52 | return await wallet.createNode({ 53 | ...opts, 54 | keyPath, 55 | parentTxID 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/node-interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Node Interface to be implemented. Any implemetation needs to implement both getUTXOs and sendRawTX. 3 | */ 4 | export default abstract class NodeInterface { 5 | /** 6 | * @param opts - Interface Options 7 | */ 8 | constructor(public opts = {}) {} 9 | 10 | /** 11 | * Should return an Array of UTXOs. 12 | * 13 | * @param address - Address 14 | */ 15 | public abstract async getUTXOs(address: string): Promise; 16 | 17 | /** 18 | * Should return a response containing the txid if successful. 19 | * 20 | * @param tx - Raw transaction string 21 | */ 22 | public abstract async sendRawTX(tx: string): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /src/node-interfaces/bitindex.ts: -------------------------------------------------------------------------------- 1 | import { instance } from "bitindex-sdk"; 2 | import NodeInterface from "../node-interface"; 3 | 4 | interface IApiOptions { 5 | apiKey?: string; 6 | } 7 | 8 | /** 9 | * Implements decrecated BitIndex API Interface. 10 | */ 11 | export class BitIndex extends NodeInterface { 12 | get instance(this) { 13 | return instance(); 14 | } 15 | 16 | public async getUTXOs(address: string) { 17 | return await this.instance.address.getUtxos(address); 18 | } 19 | 20 | public async sendRawTX(tx: string) { 21 | return await this.instance.tx.send(tx); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/node-interfaces/mattercloud.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { instance } from "mattercloudjs"; 3 | import NodeInterface from "../node-interface"; 4 | 5 | interface IApiOptions { 6 | apiKey?: string; 7 | } 8 | 9 | /** 10 | * Implements MatterCloud API Interface. 11 | * The Mattercloud API requires an api key. By default the public key from the wallet is used. 12 | */ 13 | export class Mattercloud extends NodeInterface { 14 | public static async requestApiKey(): Promise { 15 | const response = await axios.post("https://api.bitindex.network/api/v2/registration/account?secret=secretkey"); 16 | const apiKey = response.data && response.data.apiKey; 17 | if (!apiKey) { 18 | throw new Error("Failed to request MatterCloud API key."); 19 | } 20 | return apiKey; 21 | } 22 | 23 | constructor(public opts: IApiOptions = {}) { 24 | super(opts); 25 | } 26 | 27 | get instance(this) { 28 | return instance({ api_key: this.opts.apiKey }); 29 | } 30 | 31 | public async init() { 32 | if (!this.opts.apiKey) { 33 | this.opts.apiKey = await Mattercloud.requestApiKey(); 34 | } 35 | } 36 | 37 | public async getUTXOs(address: string) { 38 | await this.init(); 39 | return await this.instance.getUtxos(address, {}); 40 | } 41 | 42 | public async sendRawTX(tx: string) { 43 | await this.init(); 44 | return await this.instance.sendRawTx(tx); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/planter.ts: -------------------------------------------------------------------------------- 1 | import bsv from "bsv"; 2 | import { TreeHugger } from "./index"; 3 | import MetaNode from "./meta-node"; 4 | import NodeInterface from "./node-interface"; 5 | import { BitIndex } from "./node-interfaces/bitindex"; 6 | import { getRandomKeyPath } from "./utils"; 7 | 8 | const { Buffer } = bsv.deps; 9 | 10 | const defaults = { 11 | feeb: 1.4, 12 | minimumOutputValue: 546 13 | }; 14 | 15 | interface IOptions { 16 | xprivKey?: string; 17 | nodeInterface?: NodeInterface; 18 | feeb?: number; 19 | } 20 | 21 | export interface INodeOptions { 22 | data?: string[]; 23 | parentTxID?: string; 24 | parentKeyPath?: string; 25 | keyPath?: string; 26 | safe?: boolean; 27 | includeKeyPath?: boolean; 28 | } 29 | 30 | interface IScriptOptions { 31 | data?: string[]; 32 | keyPath?: string; 33 | address: string; 34 | parentTxID?: string; 35 | safe?: boolean; 36 | } 37 | 38 | export class Planter { 39 | public xprivKey: bsv.HDPrivateKey; 40 | public feeb: number; 41 | public nodeInterface: NodeInterface; 42 | private spendInputs: bsv.Transaction.Output[]; 43 | private query: object; 44 | 45 | constructor({ xprivKey, nodeInterface = new BitIndex(), feeb = defaults.feeb }: IOptions = {}) { 46 | this.xprivKey = xprivKey ? bsv.HDPrivateKey.fromString(xprivKey) : bsv.HDPrivateKey.fromRandom(); 47 | this.query = { 48 | "in.tape.cell.b": this.encodedPubKey 49 | }; 50 | this.spendInputs = []; 51 | this.feeb = feeb; 52 | this.nodeInterface = nodeInterface; 53 | } 54 | 55 | get fundingAddress() { 56 | return this.xprivKey.publicKey.toAddress().toString(); 57 | } 58 | 59 | get publicKey() { 60 | return this.xprivKey.publicKey.toString(); 61 | } 62 | 63 | get encodedPubKey() { 64 | return this.xprivKey.publicKey.toDER().toString("base64"); 65 | } 66 | 67 | public async findSingleNode(request = { find: {} }, opts?): Promise { 68 | request.find = { 69 | ...request.find, 70 | ...this.query 71 | }; 72 | return await TreeHugger.findSingleNode(request, opts); 73 | } 74 | 75 | public async findAllNodes(request = { find: {} }, opts?): Promise { 76 | request.find = { 77 | ...request.find, 78 | ...this.query 79 | }; 80 | return await TreeHugger.findAllNodes(request, opts); 81 | } 82 | 83 | public async findNodeById(id, opts?): Promise { 84 | const find = { "node.id": id, ...this.query }; 85 | return await TreeHugger.findSingleNode({ find }, opts); 86 | } 87 | 88 | public async findNodeByTxid(txid, opts?): Promise { 89 | const find = { "node.tx": txid, ...this.query }; 90 | return await TreeHugger.findSingleNode({ find }, opts); 91 | } 92 | 93 | public async findNodesByAddress(addr, opts?): Promise { 94 | const find = { "node.a": addr, ...this.query }; 95 | return await TreeHugger.findAllNodes({ find }, opts); 96 | } 97 | 98 | public async findNodesByParentId(id, opts?): Promise { 99 | const find = { 100 | head: true, 101 | "parent.id": id, 102 | ...this.query 103 | }; 104 | return await TreeHugger.findAllNodes({ find }, opts); 105 | } 106 | 107 | public async findNodeAndDescendants(id, opts?): Promise { 108 | const find = { 109 | $or: [{ "node.id": id }, { "ancestor.id": id }], 110 | head: true, 111 | ...this.query 112 | }; 113 | return await TreeHugger.findAllNodes({ find }, opts); 114 | } 115 | 116 | public async createNode({ 117 | data, 118 | parentTxID, 119 | parentKeyPath, 120 | keyPath, 121 | safe = true, 122 | includeKeyPath = true 123 | }: INodeOptions = {}) { 124 | keyPath = keyPath || getRandomKeyPath(); 125 | 126 | const nodeAddress = this.xprivKey.deriveChild(keyPath).publicKey.toAddress(); 127 | 128 | const utxos = await this.nodeInterface.getUTXOs(this.fundingAddress); 129 | 130 | if (utxos.some(output => this.isSpend(output))) { 131 | return this.createNode({ data, parentTxID, parentKeyPath, keyPath, safe, includeKeyPath }); 132 | } 133 | 134 | const balance = utxos.reduce((a, c) => a + c.satoshis, 0); 135 | 136 | const scriptOptions: IScriptOptions = { 137 | address: nodeAddress.toString(), 138 | data, 139 | parentTxID, 140 | safe 141 | }; 142 | if (includeKeyPath) { 143 | scriptOptions.keyPath = keyPath; 144 | } 145 | const script = buildScript(scriptOptions); 146 | 147 | const tx = new bsv.Transaction() 148 | .from(utxos) 149 | .addOutput(new bsv.Transaction.Output({ script: script.toString(), satoshis: 0 })) 150 | .to(nodeAddress, defaults.minimumOutputValue) // Dust output for future nodes 151 | .change(this.fundingAddress); 152 | 153 | const privateKeys = [this.xprivKey.privateKey.toString()]; 154 | 155 | if (parentTxID) { 156 | if (!parentKeyPath) { 157 | const parent = await this.findNodeByTxid(parentTxID); 158 | 159 | if (!parent) { 160 | throw new Error("Unable to find parent node"); 161 | } 162 | 163 | parentKeyPath = parent.keyPath; 164 | if (!parentKeyPath) { 165 | // TODO: Add option to pass parent node keyPath 166 | throw new Error("No keyPath found for parent node"); 167 | } 168 | } 169 | 170 | const parentXPrivKey = this.xprivKey.deriveChild(parentKeyPath); 171 | const parentAddress = parentXPrivKey.publicKey.toAddress().toString(); 172 | const parentUtxos = await this.nodeInterface.getUTXOs(parentAddress); 173 | const parentPrivKey = parentXPrivKey.privateKey.toString(); 174 | 175 | if (parentUtxos.length === 0) { 176 | // TODO: Optionally create funding tx 177 | throw new Error("Missing dust outputs for parent node. Create transction to parent node first."); 178 | } 179 | 180 | tx.from(parentUtxos); 181 | tx.to(parentAddress, defaults.minimumOutputValue); // Dust output for future nodes 182 | privateKeys.push(parentPrivKey); 183 | } 184 | 185 | const fee = Math.ceil(tx._estimateSize() * this.feeb); 186 | 187 | if (balance < fee) { 188 | throw new Error(`Not enough money (${balance} sat < ${fee} sat)`); 189 | } 190 | 191 | tx.fee(fee); 192 | tx.sign(privateKeys); 193 | 194 | const response = await this.nodeInterface.sendRawTX(tx.toString()); 195 | 196 | if (!response.txid) { 197 | return response; 198 | } 199 | 200 | const nodeIDBuffer = Buffer.from(nodeAddress.toString() + response.txid); 201 | const nodeID = bsv.crypto.Hash.sha256(nodeIDBuffer).toString("hex"); 202 | 203 | this.spendInputs = [...this.spendInputs, ...tx.inputs]; 204 | 205 | return { 206 | ...response, 207 | address: nodeAddress.toString(), 208 | id: nodeID, 209 | keyPath 210 | }; 211 | } 212 | 213 | private isSpend(utxo): boolean { 214 | for (const spend of this.spendInputs) { 215 | if (utxo.txid === spend.prevTxId.toString("hex") && utxo.outputIndex === spend.outputIndex) { 216 | return true; 217 | } 218 | } 219 | return false; 220 | } 221 | } 222 | 223 | function buildScript({ data = [], address, keyPath, parentTxID, safe }: IScriptOptions): bsv.Script { 224 | const script = new bsv.Script(); 225 | 226 | if (safe) { 227 | script.add(bsv.Opcode.OP_FALSE); 228 | } 229 | script.add(bsv.Opcode.OP_RETURN); 230 | script.add(Buffer.from("meta")); 231 | script.add(Buffer.from(address)); 232 | const parentRef = parentTxID || "NULL"; 233 | script.add(Buffer.from(parentRef)); 234 | if (keyPath) { 235 | script.add(Buffer.from(keyPath)); 236 | } 237 | if (data) { 238 | script.add(Buffer.from("|")); 239 | } 240 | data.forEach(item => script.add(Buffer.from(item))); 241 | 242 | return script; 243 | } 244 | -------------------------------------------------------------------------------- /src/untyped.d.ts: -------------------------------------------------------------------------------- 1 | declare module "meta-tree-hugger/lib/meta-node"; 2 | declare module "meta-tree-hugger"; 3 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import bsv from "bsv"; 2 | 3 | export function getRandomKeyPath(): string { 4 | const maxLength = 9; 5 | const path = bsv.PrivateKey.fromRandom() 6 | .toBigNumber() 7 | .toString() 8 | .split("") 9 | .reduce((r, e, i) => { 10 | i % maxLength === 0 ? r.push([e]) : r[r.length - 1].push(e); 11 | return r; 12 | }, []) 13 | .map(e => e.join("")) 14 | .join("/"); 15 | return "m/" + path; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "./lib", 7 | "rootDir": "./src", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "lib": ["es2015", "dom"] 12 | }, 13 | "include": ["./src"] 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "production", 5 | entry: { 6 | planter: "./src/index.ts" 7 | }, 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx?$/, 12 | use: "ts-loader", 13 | exclude: /node_modules/ 14 | } 15 | ] 16 | }, 17 | resolve: { 18 | extensions: [".ts", ".js"] 19 | }, 20 | output: { 21 | filename: "[name].min.js", 22 | library: "planter", 23 | libraryTarget: "umd", 24 | path: path.resolve(__dirname, "dist") 25 | }, 26 | externals: { 27 | bsv: "bsv" 28 | } 29 | }; 30 | --------------------------------------------------------------------------------