├── .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 | 
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 |
--------------------------------------------------------------------------------