├── .gitignore ├── README.md ├── lib ├── bsv.d.ts ├── client.ts ├── datapay.d.ts ├── index.ts └── models │ ├── bitcoin-asset.ts │ ├── create-action.interface.ts │ ├── create-request.interface.ts │ ├── create-response.interface.ts │ ├── get-response.interface.ts │ ├── update-action.interface.ts │ ├── update-request.interface.ts │ └── update-response.interface.ts ├── package-lock.json ├── package.json ├── test ├── buildCreateAction.js ├── buildUpdateAction.js ├── create.js ├── find.js ├── getAssetMintedVersion.js ├── integration.js ├── mocha.opts ├── update.js └── validateAssetStructure.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | dist/ 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitcoinasset:// Protocol 2 | v1.0 3 | > Create and manage (non-fungible) digital assets on the Bitcoin blockchain. 4 | 5 | - Bitcom Prefix: `1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K` 6 | - Protocol Spec: https://github.com/bitcoin-asset/bitcoinassetjs 7 | 8 | NOTE: This is beta and we need your help to define the spec for 'Ownership Transfers'. 9 | 10 | ## Use Cases 11 | 12 | ##### Art and Content Ownership 13 | Create unique art, ebooks, media and more. Be able to transfer ownership of limited edition items to other users. 14 | 15 | ##### Digital Titles 16 | Create any kind of item that benefits from ownership. You can build Crypto Kitties or any other kind of digital property. The only limitation is your imagination. 17 | 18 | ## Design Goals 19 | 1. A simple and flexible way to create, update, and transfer (non-fungible) digital assets on the blockchain. 20 | 2. Use web existing standards, including the Bitcoin Data Protocol (`b://`), to maximize compatibility. 21 | 3. Efficient state and ownership resolution: The time to resolve the current owner should be done in <= O(n) operations, where n is the total number of ownership transfers of the asset during it's lifespan. The time to resolve the current state of the asset should be no more than O(1) lookups 22 | 23 | ## Installation and Usage 24 | 25 | **Installation** 26 | ```sh 27 | npm install bitcoinassetjs --save 28 | yarn add bitcoinassetjs 29 | bower install bitcoinassetjs --save 30 | ``` 31 | 32 | **Include** 33 | ```javascript 34 | // Include the library 35 | var bitcoinassetjs = require('bitcoinassetjs'); 36 | ``` 37 | 38 | ## API Endpoints and Form Asset Builder 39 | 40 | **Query UTXOs and Wallet Balances with the [BitIndex API](https://www.bitindex.network)** 41 | 42 | 43 | **Create Bitcoin Asset at [BitcoinFiles.org/new-asset](https://www.bitcoinFiles.org/new-asset)** 44 | 45 | **Retrieve Bitcoin Asset with the [BitcoinFiles API](https://www.bitcoinFiles.org)** 46 | *Example:* 47 | [https://media.bitcoinfiles.org/3b38864d0d21fb547376da5d4e77410ce8350dc51658fd2f2a36655796ca96df](https://media.bitcoinfiles.org/3b38864d0d21fb547376da5d4e77410ce8350dc51658fd2f2a36655796ca96df) 48 | 49 | Sample response (Note the content-type of `application/bitcoinasset+json`) 50 | ``` 51 | { 52 | "success":true, 53 | "data":{ 54 | "assetImmutableData":{ 55 | 56 | }, 57 | "assetDataOriginal":{ 58 | "dataUrl":"b://cc34ca3b413f6a6ac265d04ecc02061eb036b27009ac4866371795e8448299d7", 59 | "dataSchemaUrl":"b://cda5892c061afb50f09d99baa6fef40cae660cf87637faedb12ebdc52adfa167", 60 | "dataSchemaType":"http://json-schema.org/draft-07/schema#" 61 | }, 62 | "assetDataCurrent":{ 63 | "dataUrl":"b://27ffb6f4c23a54179e07b795c5441316e7b70ece416975a5aeb6af510b634c63", 64 | "dataSchemaUrl":"b://cda5892c061afb50f09d99baa6fef40cae660cf87637faedb12ebdc52adfa167", 65 | "dataSchemaType":"http://json-schema.org/draft-07/schema#", 66 | "metadataUrl":"\u0000", 67 | "metadataSchemaUrl":"\u0000", 68 | "metadataSchemaType":"\u0000" 69 | }, 70 | "blockInfoOriginal":{ 71 | "createdTime":1551154986, 72 | "blockHeight":571299, 73 | "txid":"3b38864d0d21fb547376da5d4e77410ce8350dc51658fd2f2a36655796ca96df" 74 | }, 75 | "blockInfoCurrent":{ 76 | "createdTime":null, 77 | "blockHeight":null, 78 | "txid":"3fd203ec4a3c0644d477a68777c1c198d7d29f49b5f8eca0d4a4a9c3d8c1718f" 79 | }, 80 | "assetOwnership":{ 81 | "originalOwnerAddress":"1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 82 | "currentOwnerAddress":"1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 83 | "ownershipHistoryRecords":[ 84 | 85 | ] 86 | }, 87 | "updateAddress":"1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 88 | "txid":"3b38864d0d21fb547376da5d4e77410ce8350dc51658fd2f2a36655796ca96df" 89 | } 90 | } 91 | 92 | ``` 93 | 94 | 95 | 96 | ##### Create Asset: 97 | 98 | _Simple Example_ 99 | 100 | Create minimal viable asset. 101 | 102 | ```javascript 103 | bitcoinassetjs.getClient().create({ 104 | asset: { 105 | dataUrl: 'b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c', 106 | updateAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz' 107 | }, 108 | pay: { 109 | key: 'your private key' 110 | } 111 | }); 112 | ``` 113 | 114 | _Advanced Example_ 115 | 116 | Use JSON schema to define the type of data and how to validate it. 117 | 118 | ```javascript 119 | bitcoinassetjs.getClient().create({ 120 | asset: { 121 | dataUrl: 'b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c', 122 | dataSchemaType: 'http://json-schema.org/draft-07/schema#', 123 | dataSchemaUrl: 'b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#some-namespace/schema/some-object-type/draft-01', 124 | updateAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz' 125 | }, 126 | pay: { 127 | key: 'your private key' 128 | } 129 | }); 130 | ``` 131 | 132 | #### Find Asset 133 | 134 | ```javascript 135 | // First find and retrieve the asset and validate with JSON Schema (2nd argument = true) 136 | var found = await bitcoinassetjs.getClient().find('4b541e091d6af2f60d256dfca2685f95bf2d3a9b7995595115183d37b7c5bf90', true); 137 | console.log(found); 138 | /* 139 | { 140 | success: true, 141 | data: { 142 | "txid": "4b541e091d6af2f60d256dfca2685f95bf2d3a9b7995595115183d37b7c5bf90", 143 | "assetDataCurrent": { 144 | dataUrl: 'b://9a201f03464ef345ae7d8fd49bdf09eddc52a96345358438af4f13af7f4b56fa', 145 | dataSchemaUrl: 'b://d3df63599f8e1d02d98f263f1a0427de01c95f077758c58f2d7a6bd94ea6da70', 146 | dataSchemaType: 'http://json-schema.org/draft-07/schema#', 147 | "metadataSchemaUrl": undefined, 148 | "metadataSchemaType": undefined, 149 | "metadataUrl": undefined, 150 | }, 151 | "assetDataOriginal": { 152 | dataUrl: 'b://9a201f03464ef345ae7d8fd49bdf09eddc52a96345358438af4f13af7f4b56fa', 153 | dataSchemaUrl: 'b://d3df63599f8e1d02d98f263f1a0427de01c95f077758c58f2d7a6bd94ea6da70', 154 | dataSchemaType: 'http://json-schema.org/draft-07/schema#', 155 | "metadataSchemaUrl": undefined, 156 | "metadataSchemaType": undefined, 157 | "metadataUrl": undefined, 158 | }, 159 | "blockInfoCurrent": { 160 | "blockHeight": 570305, 161 | "createdTime": 1550546281, 162 | "txid": "4b541e091d6af2f60d256dfca2685f95bf2d3a9b7995595115183d37b7c5bf90", 163 | }, 164 | "blockInfoOriginal": { 165 | "blockHeight": 570305, 166 | "createdTime": 1550546281, 167 | "txid": "4b541e091d6af2f60d256dfca2685f95bf2d3a9b7995595115183d37b7c5bf90", 168 | }, 169 | "assetImmutableData": { 170 | "immutableSchemaUrl": undefined, 171 | "immutableSchemaType": undefined, 172 | "immutableUrl": undefined 173 | }, 174 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 175 | "assetOwnership": { 176 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 177 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 178 | ownershipHistoryRecords: [] 179 | } 180 | } 181 | }); 182 | */ 183 | 184 | ``` 185 | 186 | ##### Update Asset 187 | 188 | All the data* and metadata* fields can be updated in the asset by signing a request with the *privateKey* of the `updateAddress`. 189 | 190 | The immutable* fields are forever unchangeable. 191 | 192 | ```javascript 193 | 194 | // BEWARE: All the fields that are null/undefined or not present will OVERWRITE original asset 195 | // See below on how to ensure you always have the latest version and do not inadvertantly overwrite the fields you want to keep. 196 | const result = await bitcoinassetjs.getClient().update({ 197 | txid: '172908fd19df177356e369867623bb1f9d90b8d1d55c384ebd468985d7da035e', 198 | update: { 199 | dataUrl: 'b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c', 200 | dataSchemaType: 'http://json-schema.org/draft-07/schema#', 201 | dataSchemaUrl: 'b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#some-namespace/schema/some-object-type/draft-01', 202 | metadataUrl: undefined, 203 | metadataSchemaUrl: undefined, 204 | metadataSchemaType: undefined, 205 | }, 206 | pay: { 207 | key: 'your private key' 208 | } 209 | }); 210 | ``` 211 | 212 | Updating 1 field, but keeping the existing fields, First retrieve the latest version, and then perform the update. 213 | 214 | ```javascript 215 | 216 | // First find and retrieve the asset 217 | var found = await bitcoinassetjs.getClient().find('172908fd19df177356e369867623bb1f9d90b8d1d55c384ebd468985d7da035e'); 218 | // now update the specific field you want, using Object.assign to keep the existing fields 219 | var update = await bitcoinassetjs.getClient().update({ 220 | txid: found.data.txid, 221 | update: Object.assign({}, found.data.assetDataCurrent, 222 | { 223 | metadataSchemaUrl: 'b://updated-metadataSchemaUrl' 224 | } 225 | ), 226 | pay: { 227 | key: 'your private key' 228 | } 229 | }); 230 | 231 | ``` 232 | 233 | ## Build and Test 234 | 235 | ``` 236 | npm run build 237 | npm test 238 | ``` 239 | 240 | 241 | ----------- 242 | 243 | 244 | ## Technical Trade-offs 245 | 246 | ##### Use OP_RETURN versus Script 247 | The benefit of using OP_RETURN conventions to define ownership is that it is not easy to accidentally "spend" an asset. UTXO based assets can be accidentally spent by wallets that are "unaware" that an asset exists and users can lose control of their assets. 248 | 249 | On the other hand it is extremely easy to enumerate all owned assets by an address by checking the UTXO set. 250 | 251 | The disadvantage OP_RETURN is that it is more difficult for services to know what are all the assets a given address owns a given point in time. We describe some methods below to be able to list all currently owned assets by an address below (bitdb queries). 252 | 253 | A design goal of this protocol is to be simple to implement and compatible with `b://`, therefore we opted for an OP_RETURN based solution for *Bitcoin Simple Asset Protocol 1.0*. 254 | 255 | ##### Update asset data frequently, but transfer ownership occasionally 256 | It is expected that asset data will be updated very frequently, but ownership will be transferred infrequently. 257 | 258 | This protocol optimizes for resolving the current owner quickly and then performing a simple query to retrieve the latest version of the asset data. 259 | 260 | The reason for this is that most of the time we only care about *who is the current owner* and what is the *current state of the asset*. 261 | 262 | ##### Prefer full ownership validation 263 | To make the protocol simple and easy to implement, the rule is that all implementors must always validate the full ownership chain of an asset starting from the txid of the asset and following the CREATE transaction through all TRANSFER commands. 264 | 265 | ## Protocol Messages 266 | 267 | ### Overview 268 | ##### Create Asset 269 | Define the initial data and metadata of the asset. 270 | 271 | input1: Asset Creator 272 | output1: OP_RETURN 273 | ``` 274 | OP_RETURN 275 | 1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K 276 | 277 | 278 | 279 | [Asset Data Schema URL] 280 | [Asset Data Schema Type] 281 | [Asset Metadata URL] 282 | [Asset Metadata Schema URL] 283 | [Asset Metadata Schema Type] 284 | [Asset Immutable Data URL] 285 | [Asset Immutable Schema URL] 286 | [Asset Immutable Schema Type] 287 | [Tag 1] 288 | [Tag 2] 289 | [Tag 3] 290 | [Tag 4] 291 | [Tag 5] 292 | ``` 293 | 294 | #### Get the current state of an asset 295 | 296 | First resolve the current owner, and saving all of the Update Delegate Address along the way into an array. 297 | 298 | Starting with the most recent Update Delegate Address, query for an *Update* OP_RETURN and take the most recent transaction (highest block height) then use that as the current data. 299 | 300 | If no transaction found for current owner, then use the previous owner's update delegate and repeat the process until an Update is found. If not updates are found, then the *Create* data is the current data. 301 | 302 | Run-time: O(3 * N * k) = O(N) 303 | Where 304 | N = total number of owners 305 | k = Number of transactions each owner did with same owner address 306 | 307 | It is recommended that a new owner perform an immediate *Update* operation to easy the burden of resolving the current state for all viewers of the data. 308 | 309 | ##### Update Asset 310 | Update the data or metadata of the asset 311 | 312 | ``` 313 | OP_RETURN 314 | 1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K 315 | 316 | 317 | 318 | [Asset Data Schema URL] 319 | [Asset Data Schema Type] 320 | [Asset Metadata URL] 321 | [Asset Metadata Schema URL] 322 | [Asset Metadata Schema Type] 323 | [Tag 1] 324 | [Tag 2] 325 | [Tag 3] 326 | [Tag 4] 327 | [Tag 5] 328 | ``` 329 | 330 | **Protocol Identifier: 1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K** 331 | Fixed unique identifier to indicate that this transaction is a *Bitcoin Simple Asset* transaction. 332 | 333 | **** 334 | Required. Set to `0` for the CREATE action. 335 | Specifies whether this is a CREATE, UPDATE, or TRANSFER action. 336 | ``` 337 | enum ActionType { 338 | CREATE = 0, 339 | UPDATE = 1, 340 | TRANSFER = 2 341 | } 342 | ``` 343 | 344 | **** 345 | Required. 346 | The address that is allowed to update the asset 347 | 348 | **** 349 | Required. 350 | URL of initial state data. Can be any valid URL or Data-URL. It is recommended that a Data-URL is used or `b://` URL so that all initial state is stored on the blockchain. 351 | 352 | **[Data Schema URL]** 353 | Optional, but recommended. 354 | 355 | URL to a file that describes the types and meaning of the data. 356 | 357 | This can be a human-readable document that describes how to interpret the data located at or it can be a URL to a XML-Schema, JSON-Schema, or any other validation scheme that will validate the contents. 358 | 359 | It is recommended that a Data-URL is used or `b://` URL so that the Data Description for the initial state data is stored on the blockchain. 360 | 361 | **[Data Schema Type]** 362 | Optional, but recommended. 363 | 364 | Specifies the type of validation to perform on the data at the [Data URL] according to the rules in [Data Description URL] 365 | 366 | To use JSON-Schema, set to point to a file with content-type `application/json` and to `http://json-schema.org/schema#` 367 | 368 | To use XML-Schema, set to point to a file with content-type `application/xml` and to `http://www.w3.org/2001/XMLSchema`. 369 | 370 | Any type of programmatic or human readable validation can be used. If a human-readable text description is desired, then the can be omitted and then rely on humans interpretting the content at [Data Description URL] 371 | 372 | ### Get current asset data 373 | 374 | First resolve the current owner. 375 | 376 | Then perform a query to fetch the most recent *UPDATE* action created by the current owner. 377 | 378 | Todo: How do we get the correct recent state even if the owner of the asset has exchanged hands back and forth with the same address? Note: Using the above logic, we will get stale state if some other intermediate owner changed the state 379 | 380 | # Future / Next Steps 381 | 382 | #### How to do Atomic Swap of 2 assets? 383 | TODO 384 | 385 | #### How can one asset "own" another asset? 386 | TODO 387 | 388 | #### How to transfer ownership 389 | 390 | - Allow atomic swaps between 2 or more bitcoin assets 391 | - Make it safe and secure for buyer and seller to send money to pay for an asset and have it atomically transact (or not go through at all) 392 | 393 | -------------------------------------------------------------------------------- /lib/bsv.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'bsv'; 2 | -------------------------------------------------------------------------------- /lib/client.ts: -------------------------------------------------------------------------------- 1 | import * as datapay from 'datapay'; 2 | import * as Base58 from 'base-58'; 3 | import axios from 'axios'; 4 | import { CreateRequest } from './models/create-request.interface'; 5 | import { CreateResponse } from './models/create-response.interface'; 6 | import { GetResponse } from './models/get-response.interface'; 7 | import { BitcoinAsset } from './models/bitcoin-asset'; 8 | import { UpdateResponse } from './models/update-response.interface'; 9 | import { UpdateRequest } from './models/update-request.interface'; 10 | import * as jsonschema from 'jsonschema'; 11 | 12 | declare var Buffer: any; 13 | 14 | function buf2hex(buffer: any) { 15 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('') 16 | } 17 | 18 | const defaultOptions = { 19 | // bitdb.network is used to query for the asset 20 | bitdb_api_base: 'https://babel.bitdb.network/q/1DHDifPvtPgKFPZMRSxmVHhiPvFmxZwbfh/', 21 | bitdb_api_key: '12cHytySdrQGRtuvvkVde2j3e74rmEn5TM', 22 | // bitcoinfiles.org is used to retrieve b:// protocol assets 23 | b_api_base: 'https://media.bitcoinfiles.org', 24 | } 25 | 26 | export class Client { 27 | options = defaultOptions; 28 | constructor(options: any) { 29 | this.options = Object.assign({}, this.options, options); 30 | } 31 | 32 | callbackAndResolve(resolveOrReject: Function, data: any, callback?: Function) { 33 | if (callback) { 34 | callback(data); 35 | } 36 | return resolveOrReject(data); 37 | } 38 | 39 | /** 40 | * Create an asset 41 | * @param createRequest Creation request 42 | */ 43 | create(createRequest: CreateRequest, callback?: Function): Promise{ 44 | return new Promise((resolve, reject) => { 45 | if (!createRequest.pay || !createRequest.pay.key || createRequest.pay.key === '') { 46 | return this.callbackAndResolve(resolve, { 47 | success: false, 48 | message: 'key required' 49 | }, callback); 50 | } 51 | 52 | if (!createRequest.asset) { 53 | return this.callbackAndResolve(resolve, { 54 | success: false, 55 | message: 'asset required' 56 | }, callback); 57 | } 58 | 59 | if (!createRequest.asset.dataUrl) { 60 | return this.callbackAndResolve(resolve, { 61 | success: false, 62 | message: 'dataUrl required' 63 | }, callback); 64 | } 65 | 66 | if (!createRequest.asset.updateAddress) { 67 | return this.callbackAndResolve(resolve, { 68 | success: false, 69 | message: 'updateAddress required' 70 | }, callback); 71 | } 72 | const args = this.buildCreateAction(createRequest); 73 | 74 | datapay.send({ 75 | data: args, 76 | pay: { 77 | key: createRequest.pay.key 78 | } 79 | }, async (err: any, transaction: any) => { 80 | if (err) { 81 | console.log('err', err); 82 | return this.callbackAndResolve(resolve, { 83 | success: false, 84 | message: err.message ? err.message : err.toString() 85 | }, callback); 86 | } 87 | return this.callbackAndResolve(resolve, { 88 | success: true, 89 | data: { 90 | txid: transaction 91 | } 92 | }, callback) 93 | }); 94 | }); 95 | } 96 | /** 97 | * 98 | * @param txid Asset identifier 99 | */ 100 | private getAssetMintedVersion(txid: string, callback?: Function): Promise { 101 | return new Promise((resolve, reject) => { 102 | if (!txid || txid === '') { 103 | return this.callbackAndResolve(resolve, { 104 | success: false, 105 | message: 'txid required' 106 | }, callback); 107 | } 108 | const query = JSON.stringify({ 109 | "v": 3, 110 | "q": { 111 | "find": { 112 | "out.s1": "1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K", 113 | "out.h2": "00", 114 | "tx.h": txid 115 | }, 116 | "limit": 1, 117 | }, 118 | "r": { 119 | "f": "[.[] | { txid: .tx.h, inputInfo: . | { in: .in? }, blockInfo: . | { blockIndex: .blk.i?, blockTime: .blk.t?}, out: .out } ]" 120 | } 121 | }); 122 | 123 | return axios.get( 124 | this.options.bitdb_api_base + Buffer.from(query).toString('base64'), 125 | { 126 | headers: { key: this.options.bitdb_api_key } 127 | } 128 | ).then((response) => { 129 | if (!response.data) { 130 | return this.callbackAndResolve(resolve, { 131 | success: false, 132 | message: "no data" 133 | }, callback); 134 | } 135 | if (!response.data.u.length && !response.data.c.length) { 136 | return this.callbackAndResolve(resolve, { 137 | success: false, 138 | message: "tx not found" 139 | }, callback); 140 | } 141 | 142 | let tx = response.data.c.length ? response.data.c[0] : response.data.u.length ? response.data.u[0] : undefined; 143 | 144 | if (!tx) { 145 | console.log('tx', tx); 146 | throw new Error('not found tx'); 147 | } 148 | 149 | return this.callbackAndResolve(resolve, { 150 | success: true, 151 | data: BitcoinAsset.validateAssetStructure(BitcoinAsset.buildBitcoinAssetFromBitDbTxResult(tx)) 152 | }, callback); 153 | }); 154 | }); 155 | } 156 | 157 | private findUpdate(getResponse: any, txid: string) { 158 | const query = JSON.stringify({ 159 | "v": 3, 160 | "q": { 161 | "find": { 162 | "in.e.a": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 163 | "out.s1": "1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K", 164 | "out.h2": "01", 165 | "out.s3": txid, 166 | }, 167 | "limit": 1, 168 | "sort": { "blk.i": -1 } 169 | }, 170 | "r": { 171 | "f": "[.[] | { txid: .tx.h, inputInfo: . | { in: .in? }, blockInfo: . | { blockIndex: .blk.i?, blockTime: .blk.t?}, out: .out } ]" 172 | } 173 | }); 174 | return axios.get( 175 | this.options.bitdb_api_base + Buffer.from(query).toString('base64'), 176 | { 177 | headers: { key: this.options.bitdb_api_key } 178 | } 179 | ).then((response) => { 180 | let tx = response.data.u.length ? response.data.u[0] : response.data.c.length ? response.data.c[0] : undefined; 181 | if (!tx) { 182 | return getResponse; // We have the latest version already! 183 | } 184 | const originalAsset = getResponse.data; 185 | originalAsset.updateWithLatestDataFromBitDbQuery(tx); 186 | const updatedAssetReply = Object.assign({}, getResponse, { 187 | data: originalAsset 188 | }); 189 | return updatedAssetReply; 190 | }); 191 | } 192 | 193 | private downloadAndValidate(getResponse: any): Promise { 194 | let dataUrl = getResponse.data.assetDataCurrent.dataUrl; 195 | let dataSchemaUrl = getResponse.data.assetDataCurrent.dataSchemaUrl; 196 | const urlRegex = /^(https?|bitcoinasset|b):\/\/(.+)/i; 197 | const bRegex = /^(bitcoinasset|b):\/\/(.+)?#?/i; 198 | 199 | if (!urlRegex.test(dataUrl) || !urlRegex.test(dataSchemaUrl)) { 200 | // Not a URL, skip it 201 | return getResponse; 202 | } 203 | 204 | const dataUrlMatch = bRegex.exec(dataUrl); 205 | if (dataUrlMatch) { 206 | dataUrl = `https://media.bitcoinfiles.org/${dataUrlMatch[2]}`; 207 | } 208 | 209 | const dataSchemaUrlMatch = bRegex.exec(dataSchemaUrl); 210 | if (dataSchemaUrlMatch) { 211 | dataSchemaUrl = `https://media.bitcoinfiles.org/${dataSchemaUrlMatch[2]}`; 212 | } 213 | 214 | // We now have 2 valid URLs 215 | return axios.get(dataUrl).then((response) => { 216 | if (!response.headers['content-type'] || !/(text|application)\/(javascript|json)/i.test(response.headers['content-type'])) { 217 | throw new Error('invalid content type for referenced b:// files in dataUrl'); 218 | } 219 | return response.data; 220 | }).then((dataUrlResponseData) => { 221 | return axios.get(dataSchemaUrl).then((schemaResponse) => { 222 | if (!schemaResponse.headers['content-type'] || !/(text|application)\/(javascript|json)/i.test(schemaResponse.headers['content-type'])) { 223 | throw new Error('invalid content type for referenced b:// files in dataSchemaUrl'); 224 | } 225 | const schemaValidator = new jsonschema.Validator(); 226 | const validationResult = schemaValidator.validate(dataUrlResponseData, schemaResponse.data); 227 | if (validationResult.errors.length) { 228 | throw new Error(`schema validation failed for data ${dataUrl} with schema ${dataSchemaUrl}`); 229 | } 230 | return getResponse; 231 | }); 232 | }); 233 | } 234 | 235 | async find(txid: string, validate: boolean = false): Promise { 236 | return await this.getAssetMintedVersion(txid).then((getResponse: any) => { 237 | if (!getResponse) { 238 | return { 239 | success: false, 240 | message: 'undefined' 241 | } 242 | } 243 | if (!getResponse || !getResponse.success || !getResponse.data) { 244 | return getResponse; 245 | } 246 | 247 | return this.findUpdate(getResponse, txid).then((updatedResponse) => { 248 | if (!updatedResponse.success) { 249 | return updatedResponse; 250 | } 251 | // Perform validation if and only if it's known 252 | if (validate && /https?\:\/\/json\-schema\.org\/draft\-07\/schema/i.test(updatedResponse.data.assetDataCurrent.dataSchemaType)) { 253 | return this.downloadAndValidate(updatedResponse); 254 | } 255 | return updatedResponse; 256 | }) 257 | }); 258 | } 259 | 260 | buildCreateAction(createRequest: CreateRequest): any[] { 261 | const updateAddressHex = buf2hex(Base58.decode(createRequest.asset.updateAddress)); 262 | const args = [ 263 | '1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K', 264 | '0x00', 265 | '0x' + updateAddressHex 266 | ]; 267 | const fields = [ 268 | 'dataUrl', 269 | 'dataSchemaUrl', 270 | 'dataSchemaType', 271 | 'metadataUrl', 272 | 'metadataSchemaUrl', 273 | 'metadataSchemaType', 274 | 'immutableUrl', 275 | 'immutableSchemaUrl', 276 | 'immutableSchemaType', 277 | ]; 278 | 279 | for (const field of fields) { 280 | if (createRequest.asset[field]) { 281 | const encodedHex = buf2hex(Buffer.from(createRequest.asset[field], 'utf8')) 282 | args.push(`0x${encodedHex}`); 283 | } else { 284 | args.push(`0x00`); 285 | } 286 | } 287 | return args; 288 | } 289 | 290 | buildUpdateAction(updateRequest: UpdateRequest): any[] { 291 | const args = [ 292 | '1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K', 293 | '0x01', 294 | ]; 295 | 296 | const txIdEncodedHex = buf2hex(Buffer.from(updateRequest.txid, 'utf8')) 297 | args.push(`0x${txIdEncodedHex}`); 298 | 299 | const fields = [ 300 | 'dataUrl', 301 | 'dataSchemaUrl', 302 | 'dataSchemaType', 303 | 'metadataUrl', 304 | 'metadataSchemaUrl', 305 | 'metadataSchemaType' 306 | ]; 307 | 308 | for (const field of fields) { 309 | if (updateRequest.update[field]) { 310 | const encodedHex = buf2hex(Buffer.from(updateRequest.update[field], 'utf8')) 311 | args.push(`0x${encodedHex}`); 312 | } else { 313 | args.push(`0x00`); 314 | } 315 | } 316 | return args; 317 | } 318 | 319 | async update(updateRequest: UpdateRequest, callback?: Function): Promise{ 320 | return new Promise((resolve, reject) => { 321 | 322 | if (!updateRequest || !updateRequest.pay || !updateRequest.pay.key || updateRequest.pay.key === '') { 323 | return this.callbackAndResolve(resolve, { 324 | success: false, 325 | message: 'key required' 326 | }, callback); 327 | } 328 | 329 | if (!updateRequest.update) { 330 | return this.callbackAndResolve(resolve, { 331 | success: false, 332 | message: 'update required' 333 | }, callback); 334 | } 335 | 336 | if (!updateRequest.txid) { 337 | return this.callbackAndResolve(resolve, { 338 | success: false, 339 | message: 'txid required' 340 | }, callback); 341 | } 342 | 343 | if (!updateRequest.update.dataUrl && 344 | !updateRequest.update.dataSchemaUrl && 345 | !updateRequest.update.dataSchemaType && 346 | !updateRequest.update.metadataUrl && 347 | !updateRequest.update.metadataSchemaUrl && 348 | !updateRequest.update.metadataSchemaType) { 349 | return this.callbackAndResolve(resolve, { 350 | success: false, 351 | message: 'update.dataUrl required or update.dataSchemaUrl required or update.dataSchemaType update.metadataUrl required or update.metadataSchemaUrl required or update.metadataSchemaType required' 352 | }, callback); 353 | } 354 | 355 | const args = this.buildUpdateAction(updateRequest); 356 | 357 | datapay.send({ 358 | data: args, 359 | pay: { 360 | key: updateRequest.pay.key 361 | } 362 | }, (err: any, transaction: any) => { 363 | if (err) { 364 | this.callbackAndResolve(resolve, { 365 | success: false, 366 | message: err.message ? err.message : err.toString() 367 | }, callback); 368 | } 369 | this.callbackAndResolve(resolve, { 370 | success: true, 371 | data: { 372 | txid: transaction 373 | } 374 | }, callback); 375 | }); 376 | }); 377 | } 378 | } -------------------------------------------------------------------------------- /lib/datapay.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'datapay'; 2 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Client } from './client'; 3 | import { BitcoinAsset } from './models/bitcoin-asset'; 4 | 5 | /** 6 | * Create a client to access network 7 | * @param options Options 8 | */ 9 | export function getClient(options: any): Client { 10 | return new Client(options); 11 | } 12 | 13 | /** 14 | * Decode a b:// or bitcoinasset:// URL to a hosting provider 15 | * @param url The url to decode. Correctly decodes b:// and bitcoinasset:// URLs and replaces it with the provided fileHostBase 16 | * @param fileHostBase Default: 'https://media.bitcoinfiles.org/' 17 | */ 18 | export function decodeBitcoinDataUrl(url: string, fileHostBase = 'https://media.bitcoinfiles.org/'): string { 19 | const bRegex = /^(bitcoinasset|b):\/\/(.+)?#?/i; 20 | const match = bRegex.exec(url); 21 | if (match) { 22 | return `${fileHostBase}${match[2]}`; 23 | } 24 | // Else return if there was no match 25 | return url; 26 | } 27 | 28 | /** 29 | * Get static bitcoin asset functions 30 | */ 31 | export function getBitcoinAsset(): any { 32 | return BitcoinAsset; 33 | } 34 | -------------------------------------------------------------------------------- /lib/models/bitcoin-asset.ts: -------------------------------------------------------------------------------- 1 | import * as Base58 from 'base-58'; 2 | 3 | declare var Buffer; 4 | 5 | export interface BlockInfo { 6 | createdTime?: number; 7 | blockHeight?: number; 8 | txid: string; 9 | }; 10 | 11 | export interface BitcoinAssetDataImmutablePayload { 12 | immutableUrl?: string; 13 | immutableSchemaUrl?: string; 14 | immutableSchemaType?: string; 15 | } 16 | 17 | export interface BitcoinAssetDataPayload { 18 | dataUrl: string; 19 | dataSchemaUrl?: string; 20 | dataSchemaType?: string; 21 | metadataUrl?: string; 22 | metadataSchemaUrl?: string; 23 | metadataSchemaType?: string; 24 | } 25 | 26 | export interface BitcoinAssetOwnership { 27 | originalOwnerAddress: string; 28 | currentOwnerAddress: string; 29 | ownershipHistoryRecords: Array; // todo 30 | } 31 | export class BitcoinAsset { 32 | 33 | public txid: string; 34 | private assetImmutableData?: BitcoinAssetDataImmutablePayload = undefined; 35 | private assetDataOriginal?: BitcoinAssetDataPayload = undefined; 36 | private assetDataCurrent?: BitcoinAssetDataPayload = undefined; 37 | private blockInfoOriginal?: BlockInfo = undefined; 38 | private blockInfoCurrent?: BlockInfo = undefined; 39 | private assetOwnership?: BitcoinAssetOwnership = undefined; 40 | private updateAddress?: string = undefined; 41 | 42 | constructor( 43 | txid: string, 44 | assetDataOriginal: BitcoinAssetDataPayload, 45 | assetDataCurrent: BitcoinAssetDataPayload, 46 | blockInfoOriginal: BlockInfo, 47 | blockInfoCurrent: BlockInfo, 48 | assetOwnership: BitcoinAssetOwnership, 49 | updateAddress: string, 50 | assetImmutableData?: BitcoinAssetDataImmutablePayload 51 | ) { 52 | this.txid = txid; 53 | this.assetDataOriginal = assetDataOriginal; 54 | this.assetDataCurrent = assetDataCurrent; 55 | this.blockInfoOriginal = blockInfoOriginal; 56 | this.blockInfoCurrent = blockInfoCurrent; 57 | this.assetOwnership = assetOwnership; 58 | this.updateAddress = updateAddress; 59 | this.assetImmutableData = assetImmutableData; 60 | 61 | if ( 62 | !this.txid || 63 | !this.assetDataOriginal || 64 | !this.assetDataCurrent || 65 | !this.blockInfoOriginal || 66 | !this.blockInfoCurrent || 67 | !this.assetOwnership || 68 | !this.updateAddress 69 | ) { 70 | throw new Error('initialize asset error'); 71 | } 72 | } 73 | 74 | static buildBitcoinAssetFromBitDbTxResult(tx: any): BitcoinAsset { 75 | return new BitcoinAsset( 76 | tx.txid, 77 | { 78 | dataUrl: tx.out[0].s4 === '\u0000' ? undefined : tx.out[0].s4, 79 | dataSchemaUrl: tx.out[0].s5 === '\u0000' ? undefined : tx.out[0].s5, 80 | dataSchemaType: tx.out[0].s6 === '\u0000' ? undefined : tx.out[0].s6, 81 | metadataUrl: tx.out[0].s7 === '\u0000' ? undefined : tx.out[0].s7, 82 | metadataSchemaUrl: tx.out[0].s8 === '\u0000' ? undefined : tx.out[0].s8, 83 | metadataSchemaType: tx.out[0].s9 === '\u0000' ? undefined : tx.out[0].s9, 84 | }, 85 | { 86 | dataUrl: tx.out[0].s4 === '\u0000' ? undefined : tx.out[0].s4, 87 | dataSchemaUrl: tx.out[0].s5 === '\u0000' ? undefined : tx.out[0].s5, 88 | dataSchemaType: tx.out[0].s6 === '\u0000' ? undefined : tx.out[0].s6, 89 | metadataUrl: tx.out[0].s7 === '\u0000' ? undefined : tx.out[0].s7, 90 | metadataSchemaUrl: tx.out[0].s8 === '\u0000' ? undefined : tx.out[0].s8, 91 | metadataSchemaType: tx.out[0].s9 === '\u0000' ? undefined : tx.out[0].s9, 92 | }, 93 | { 94 | createdTime: tx.blockInfo ? tx.blockInfo.blockTime : undefined, 95 | blockHeight: tx.blockInfo ? tx.blockInfo.blockIndex : undefined, 96 | txid: tx.txid, 97 | }, 98 | { 99 | createdTime: tx.blockInfo ? tx.blockInfo.blockTime : undefined, 100 | blockHeight: tx.blockInfo ? tx.blockInfo.blockIndex : undefined, 101 | txid: tx.txid, 102 | }, 103 | { 104 | originalOwnerAddress: tx.inputInfo.in[0].e.a, 105 | currentOwnerAddress: tx.inputInfo.in[0].e.a, 106 | ownershipHistoryRecords: [] 107 | }, 108 | Base58.encode(Buffer.from(tx.out[0].h3, 'hex')), 109 | { 110 | immutableUrl: tx.out[0].s10 === '\u0000' ? undefined : tx.out[0].s10, 111 | immutableSchemaUrl: tx.out[0].s11 === '\u0000' ? undefined : tx.out[0].s11, 112 | immutableSchemaType: tx.out[0].s12 === '\u0000' ? undefined : tx.out[0].s12, 113 | } 114 | ); 115 | } 116 | 117 | updateWithLatestDataFromBitDbQuery(tx: any) { 118 | this.assetDataCurrent = Object.assign({}, this.assetDataCurrent, { 119 | dataUrl: tx.out[0].s4, 120 | dataSchemaUrl: tx.out[0].s5, 121 | dataSchemaType: tx.out[0].s6, 122 | metadataUrl: tx.out[0].s7, 123 | metadataSchemaUrl: tx.out[0].s8, 124 | metadataSchemaType: tx.out[0].s9, 125 | }); 126 | 127 | this.blockInfoCurrent = Object.assign({}, this.blockInfoCurrent, { 128 | blockHeight: tx.blockInfo ? tx.blockInfo.blockIndex : undefined, 129 | createdTime: tx.blockInfo ? tx.blockInfo.blockTime : undefined, 130 | txid: tx.txid, 131 | }); 132 | } 133 | 134 | getId(): string { 135 | if (!this.txid) { 136 | throw Error('incomplete id'); 137 | } 138 | return this.txid; 139 | } 140 | 141 | getAssetOwnership(): BitcoinAssetOwnership { 142 | if (!this.assetOwnership) { 143 | throw Error('incomplete asset'); 144 | } 145 | return this.assetOwnership; 146 | } 147 | 148 | getUpdateAddress(): string { 149 | if (!this.updateAddress) { 150 | throw Error('incomplete asset'); 151 | } 152 | return this.updateAddress; 153 | } 154 | 155 | getAssetDataImmutable(): BitcoinAssetDataImmutablePayload | undefined { 156 | return this.assetImmutableData; 157 | } 158 | 159 | getAssetDataOriginal(): BitcoinAssetDataPayload { 160 | if (!this.assetDataOriginal) { 161 | throw Error('incomplete asset'); 162 | } 163 | return this.assetDataOriginal; 164 | } 165 | 166 | getAssetDataCurrent(): BitcoinAssetDataPayload { 167 | if (!this.assetDataCurrent) { 168 | throw Error('incomplete asset'); 169 | } 170 | return this.assetDataCurrent; 171 | } 172 | 173 | getMetadataOriginal(): BlockInfo | undefined { 174 | return this.blockInfoOriginal; 175 | } 176 | 177 | getAssetMetadataCurrent(): BlockInfo | undefined { 178 | return this.blockInfoCurrent; 179 | } 180 | 181 | static validateAssetStructure(raw: any): BitcoinAsset { 182 | if (!raw) { 183 | throw new Error('undefined asset'); 184 | } 185 | if (!raw.assetDataCurrent) { 186 | throw new Error('undefined assetDataCurrent'); 187 | } else { 188 | if (!raw.assetDataCurrent.dataUrl) { 189 | throw new Error('undefined assetDataCurrent.dataUrl'); 190 | } 191 | } 192 | 193 | if (!raw.assetDataOriginal) { 194 | throw new Error('undefined assetDataOriginal'); 195 | } else { 196 | if (!raw.assetDataOriginal.dataUrl) { 197 | throw new Error('undefined assetDataOriginal.dataUrl'); 198 | } 199 | } 200 | 201 | if (!raw.blockInfoCurrent) { 202 | throw new Error('undefined blockInfoCurrent'); 203 | } 204 | 205 | if (!raw.blockInfoOriginal) { 206 | throw new Error('undefined blockInfoOriginal'); 207 | } 208 | 209 | if (!raw.txid || raw.txid === '') { 210 | throw new Error('undefined txid'); 211 | } 212 | 213 | if (!raw.updateAddress || raw.updateAddress === '') { 214 | throw new Error('undefined updateAddress'); 215 | } 216 | 217 | if (!raw.assetOwnership) { 218 | throw new Error('undefined assetOwnership'); 219 | } 220 | 221 | if (!raw.assetOwnership.originalOwnerAddress) { 222 | throw new Error('undefined assetOwnership.originalOwnerAddress'); 223 | } 224 | 225 | if (!raw.assetOwnership.currentOwnerAddress) { 226 | throw new Error('undefined assetOwnership.currentOwnerAddress'); 227 | } 228 | 229 | return new BitcoinAsset( 230 | raw.txid, 231 | raw.assetDataOriginal, 232 | raw.assetDataCurrent, 233 | raw.blockInfoOriginal, 234 | raw.blockInfoCurrent, 235 | raw.assetOwnership, 236 | raw.updateAddress, 237 | raw.assetImmutableData 238 | ) 239 | } 240 | } 241 | /* 242 | BitcoinAsset.find('txid', true) 243 | .then((findResult) => { 244 | 245 | }).catch((error) => { 246 | console.log('error', error.message); 247 | }); 248 | 249 | BitcoinAsset.findAll(['tx1', 'tx2'], true) 250 | .then((findResult) => { 251 | 252 | }).catch((error) => { 253 | console.log('error', error.message); 254 | }); 255 | 256 | BitcoinAsset.update('txid', { data: "foo" }) 257 | .then((updateResult) => { 258 | 259 | }).catch((error) => { 260 | console.log('error', error.message); 261 | }); 262 | 263 | BitcoinAsset.getOwner('txid', { data: "foo" }) 264 | .then((updateResult) => { 265 | 266 | }).catch((error) => { 267 | console.log('error', error.message); 268 | }); 269 | 270 | BitcoinAsset.offerTo('txid', "address") 271 | .then((offerResult) => { 272 | 273 | 274 | BitcoinAsset.acceptOffer('txid', "address") 275 | .then((offerResult) => { 276 | 277 | 278 | }).catch((error) => { 279 | console.log('error', error.message); 280 | }); 281 | 282 | 283 | }).catch((error) => { 284 | console.log('error', error.message); 285 | });*/ -------------------------------------------------------------------------------- /lib/models/create-action.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CreateAction { 2 | prefix: '1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K'; 3 | action: 0; 4 | updateAddress: string; 5 | dataUrl: string; 6 | dataSchemaUrl: string; 7 | dataSchemaType: string; 8 | metadataUrl: string; 9 | } -------------------------------------------------------------------------------- /lib/models/create-request.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CreateRequest{ 2 | asset: { 3 | dataUrl: string; 4 | dataSchemaUrl?: string; 5 | dataSchemaType?: string; 6 | metadataUrl: string; 7 | metadataSchemaUrl?: string; 8 | metadataSchemaType?: string; 9 | immutableUrl: string; 10 | immutableSchemaUrl?: string; 11 | immutableSchemaType?: string; 12 | updateAddress: string; 13 | }, 14 | pay: { 15 | key: string; 16 | } 17 | } -------------------------------------------------------------------------------- /lib/models/create-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CreateResponse { 2 | success: boolean; 3 | message?: string; 4 | data?: { 5 | txid: string; 6 | } 7 | } -------------------------------------------------------------------------------- /lib/models/get-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { BitcoinAsset } from "./bitcoin-asset"; 2 | 3 | export interface GetResponse { 4 | success: boolean; 5 | message?: string; 6 | data?: BitcoinAsset; 7 | } -------------------------------------------------------------------------------- /lib/models/update-action.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateAction { 2 | prefix: '1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K'; 3 | action: 1; 4 | updateAddress: string; 5 | dataUrl: string; 6 | dataSchemaUrl: string; 7 | dataSchemaType: string; 8 | } -------------------------------------------------------------------------------- /lib/models/update-request.interface.ts: -------------------------------------------------------------------------------- 1 | import { BitcoinAsset, BitcoinAssetDataPayload } from "./bitcoin-asset"; 2 | 3 | export interface UpdateRequest{ 4 | txid: string; 5 | update: BitcoinAssetDataPayload, 6 | pay: { 7 | key: string; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/models/update-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateResponse { 2 | success: boolean; 3 | message?: string; 4 | data?: { 5 | txid: string; 6 | } 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcoinassetjs", 3 | "version": "1.0.10", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "mocha --reporter spec", 10 | "prepare": "npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/bitcoin-asset/bitcoinassetjs.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/bitcoin-asset/bitcoinassetjs/issues" 20 | }, 21 | "homepage": "https://github.com/bitcoin-asset/bitcoinassetjs#readme", 22 | "devDependencies": { 23 | "chai": "^4.2.0", 24 | "mocha": "^5.2.0", 25 | "typescript": "^3.3.3" 26 | }, 27 | "dependencies": { 28 | "axios": "^0.18.0", 29 | "base-58": "0.0.1", 30 | "datapay": "0.0.8", 31 | "jsonschema": "^1.2.4" 32 | }, 33 | "files": [ 34 | "dist/*" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /test/buildCreateAction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var index = require('../dist/index.js'); 4 | 5 | describe('buildCreateAction function test', () => { 6 | it('should buildCreateAction asset success', async () => { 7 | var args = index.getClient().buildCreateAction({ 8 | txid: '364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa', 9 | asset: { 10 | dataUrl: 'b://hello', 11 | dataSchemaUrl: undefined, 12 | dataSchemaType: undefined, 13 | updateAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz' 14 | } 15 | }); 16 | expect(args).to.eql([ 17 | "1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K", 18 | "0x00", // action create 19 | "0x009467df677dc153a88243465d09ca5fe8f7ba8cf9e10c8319", // update delegate address 20 | "0x623a2f2f68656c6c6f", // dataUrl 21 | "0x00", // dataSchemaUrl 22 | "0x00", // dataSchemaType 23 | "0x00", // metadataUrl 24 | "0x00", // metadataSchemaUrl 25 | "0x00", // metadataSchemaType 26 | "0x00", // immutableUrl 27 | "0x00", // immutableSchemaUrl 28 | "0x00" // immutableSchemaType 29 | ]); 30 | 31 | var args = index.getClient().buildCreateAction({ 32 | txid: '364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa', 33 | asset: { 34 | dataUrl: 'b://hello', 35 | dataSchemaUrl: 'b://hello123', 36 | dataSchemaType: 'b://hello456', 37 | metadataUrl: 'b://metadata', 38 | metadataSchemaUrl: 'b://metadataDesc', 39 | metadataSchemaType: 'b://metadataDescSchema', 40 | immutableUrl: 'b://immutable', 41 | immutableSchemaUrl: 'b://immutabledesc', 42 | immutableSchemaType: 'b://immutableschema', 43 | updateAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz' 44 | } 45 | }); 46 | expect(args).to.eql([ 47 | "1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K", 48 | "0x00", 49 | "0x009467df677dc153a88243465d09ca5fe8f7ba8cf9e10c8319", 50 | "0x623a2f2f68656c6c6f", 51 | "0x623a2f2f68656c6c6f313233", 52 | "0x623a2f2f68656c6c6f343536", 53 | '0x623a2f2f6d65746164617461', 54 | "0x623a2f2f6d6574616461746144657363", 55 | "0x623a2f2f6d6574616461746144657363536368656d61", 56 | "0x623a2f2f696d6d757461626c65", 57 | "0x623a2f2f696d6d757461626c6564657363", 58 | "0x623a2f2f696d6d757461626c65736368656d61" 59 | ]); 60 | }); 61 | }); -------------------------------------------------------------------------------- /test/buildUpdateAction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var index = require('../dist/index.js'); 4 | 5 | describe('buildUpdateAction function test', () => { 6 | it('should buildUpdateAction asset success', async () => { 7 | var args = index.getClient().buildUpdateAction({ 8 | txid: '364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa', 9 | update: { 10 | dataUrl: 'b://hello', 11 | dataSchemaUrl: undefined, 12 | dataSchemaType: undefined 13 | } 14 | }); 15 | expect(args).to.eql([ 16 | "1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K", 17 | "0x01", 18 | "0x33363461656361303465323531333238343730626365626237643361383737393434633663333962353235306631376362656563653838616634353966646661", 19 | "0x623a2f2f68656c6c6f", 20 | "0x00", 21 | "0x00", 22 | "0x00", 23 | "0x00", 24 | "0x00", 25 | ]); 26 | 27 | var args = index.getClient().buildUpdateAction({ 28 | txid: '364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa', 29 | update: { 30 | dataUrl: 'b://hello', 31 | dataSchemaUrl: 'b://hello123', 32 | dataSchemaType: 'b://hello456', 33 | } 34 | }); 35 | expect(args).to.eql([ 36 | "1CLcHRfBvtMVB2VNFjNXq7VfamY9FXfw7K", 37 | "0x01", 38 | "0x33363461656361303465323531333238343730626365626237643361383737393434633663333962353235306631376362656563653838616634353966646661", 39 | "0x623a2f2f68656c6c6f", 40 | "0x623a2f2f68656c6c6f313233", 41 | "0x623a2f2f68656c6c6f343536", 42 | "0x00", 43 | "0x00", 44 | "0x00", 45 | 46 | ]); 47 | }); 48 | }); -------------------------------------------------------------------------------- /test/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var index = require('../dist/index.js'); 4 | 5 | const privateKey = 'wifkeyhere'; 6 | 7 | describe('create function test', () => { 8 | it('should return false no key', async () => { 9 | var createRequest = { 10 | asset: { 11 | 12 | }, 13 | pay: { 14 | key: "" 15 | } 16 | }; 17 | 18 | var result = await index.getClient().create(createRequest); 19 | expect(result).to.eql({ 20 | success: false, 21 | message: "key required" 22 | }); 23 | 24 | createRequest = { 25 | asset: { 26 | 27 | }, 28 | pay: { 29 | key: undefined 30 | } 31 | }; 32 | 33 | var result = await index.getClient().create(createRequest); 34 | expect(result).to.eql({ 35 | success: false, 36 | message: "key required" 37 | }); 38 | }); 39 | 40 | it('should return false missing asset', async () => { 41 | var createRequest = { 42 | pay: { 43 | key: "key1" 44 | } 45 | }; 46 | 47 | var result = await index.getClient().create(createRequest); 48 | expect(result).to.eql({ 49 | success: false, 50 | message: "asset required" 51 | }); 52 | 53 | }); 54 | 55 | it('should return false missing asset callback', async () => { 56 | var createRequest = { 57 | pay: { 58 | key: "key1" 59 | } 60 | }; 61 | 62 | await index.getClient().create(createRequest, (result) => { 63 | expect(result).to.eql({ 64 | success: false, 65 | message: "asset required" 66 | }); 67 | }); 68 | }); 69 | 70 | it('should return false missing asset dataUrl', async () => { 71 | var createRequest = { 72 | asset: { 73 | 74 | }, 75 | pay: { 76 | key: "key1" 77 | } 78 | }; 79 | 80 | var result = await index.getClient().create(createRequest); 81 | expect(result).to.eql({ 82 | success: false, 83 | message: "dataUrl required" 84 | }); 85 | 86 | }); 87 | 88 | it('should return false missing asset updateAddress', async () => { 89 | var createRequest = { 90 | asset: { 91 | dataUrl: 'b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c' 92 | }, 93 | pay: { 94 | key: "key1" 95 | } 96 | }; 97 | 98 | var result = await index.getClient().create(createRequest); 99 | expect(result).to.eql({ 100 | success: false, 101 | message: "updateAddress required" 102 | }); 103 | }); 104 | 105 | /* 106 | it('should return true all required and optional properties are set', async () => { 107 | var createRequest = { 108 | asset: { 109 | dataUrl: 'b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c', 110 | dataSchemaType: 'http://json-schema.org/draft-07/schema#', 111 | dataSchemaUrl: 'b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#matter/schema/post/draft-01', 112 | updateAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz' 113 | }, 114 | pay: { 115 | key: privateKey 116 | } 117 | }; 118 | 119 | var result = await index.getClient().create(createRequest); 120 | console.log('new asset', result); 121 | expect(result.success).to.equal(true); 122 | expect(result.data.txid).to.not.be.null; 123 | });*/ 124 | /* 125 | it('should return true all required and optional properties are set for a real schema', async () => { 126 | var createRequest = { 127 | asset: { 128 | dataUrl: 'b://9a201f03464ef345ae7d8fd49bdf09eddc52a96345358438af4f13af7f4b56fa', 129 | dataSchemaUrl: 'b://d3df63599f8e1d02d98f263f1a0427de01c95f077758c58f2d7a6bd94ea6da70', 130 | dataSchemaType: 'http://json-schema.org/draft-07/schema#', 131 | updateAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz' 132 | }, 133 | pay: { 134 | key: privateKey 135 | } 136 | }; 137 | 138 | var result = await index.getClient().create(createRequest); 139 | console.log('new asset', result); 140 | expect(result.success).to.equal(true); 141 | expect(result.data.txid).to.not.be.null; 142 | });*/ 143 | 144 | }); -------------------------------------------------------------------------------- /test/find.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var index = require('../dist/index.js'); 4 | 5 | describe('find function test', () => { 6 | it('should return false no txid', async () => { 7 | var result = await index.getClient().find(); 8 | expect(result).to.eql({ 9 | success: false, 10 | message: "txid required" 11 | }); 12 | }); 13 | 14 | it('should return false txid not found', async () => { 15 | var result = await index.getClient().find('0000276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c'); 16 | expect(result).to.eql({ 17 | success: false, 18 | message: "tx not found" 19 | }); 20 | }); 21 | 22 | it('should return latest state of txid with only created once', async () => { 23 | var result = await index.getClient().find('84c43f14d73122f29a949a426c22997bc0064d67fab1cb505f194c916628a1fc'); 24 | expect(result).to.eql({ 25 | success: true, 26 | data: { 27 | "txid": "84c43f14d73122f29a949a426c22997bc0064d67fab1cb505f194c916628a1fc", 28 | "assetDataCurrent": { 29 | "dataSchemaUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#matter/schema/post/draft-01", 30 | "dataSchemaType": "http://json-schema.org/draft-07/schema#", 31 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 32 | "metadataSchemaUrl": undefined, 33 | "metadataSchemaType": undefined, 34 | "metadataUrl": undefined, 35 | }, 36 | "assetDataOriginal": { 37 | "dataSchemaUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#matter/schema/post/draft-01", 38 | "dataSchemaType": "http://json-schema.org/draft-07/schema#", 39 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 40 | "metadataSchemaUrl": undefined, 41 | "metadataSchemaType": undefined, 42 | "metadataUrl": undefined, 43 | }, 44 | "blockInfoCurrent": { 45 | "blockHeight": 570164, 46 | "createdTime": 1550468060, 47 | "txid": "84c43f14d73122f29a949a426c22997bc0064d67fab1cb505f194c916628a1fc", 48 | }, 49 | "blockInfoOriginal": { 50 | "blockHeight": 570164, 51 | "createdTime": 1550468060, 52 | "txid": "84c43f14d73122f29a949a426c22997bc0064d67fab1cb505f194c916628a1fc", 53 | }, 54 | "assetImmutableData": { 55 | "immutableSchemaUrl": undefined, 56 | "immutableSchemaType": undefined, 57 | "immutableUrl": undefined 58 | }, 59 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 60 | "assetOwnership": { 61 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 62 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 63 | ownershipHistoryRecords: [] 64 | } 65 | } 66 | }); 67 | }); 68 | 69 | it('should return latest state of txid updated', async () => { 70 | var result = await index.getClient().find('364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa'); 71 | expect(result).to.eql({ 72 | success: true, 73 | data: { 74 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 75 | "assetDataCurrent": { 76 | dataUrl: 'b://updated', 77 | dataSchemaUrl: 'b://updateddescription', 78 | dataSchemaType: 'b://dataDescriptionValidationScheme', 79 | "metadataSchemaUrl": undefined, 80 | "metadataSchemaType": undefined, 81 | "metadataUrl": undefined, 82 | }, 83 | "assetDataOriginal": { 84 | "dataSchemaUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#matter/schema/post/draft-01", 85 | "dataSchemaType": "http://json-schema.org/draft-07/schema#", 86 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 87 | "metadataSchemaUrl": undefined, 88 | "metadataSchemaType": undefined, 89 | "metadataUrl": undefined, 90 | }, 91 | "blockInfoCurrent": { 92 | "blockHeight": 570160, 93 | "createdTime": 1550465608, 94 | "txid": "ebd0cdd5c8279fd362fd948229b696244e65ca7351f5e271e0a63e9cf7bfa9e4", 95 | }, 96 | "blockInfoOriginal": { 97 | "blockHeight": 570023, 98 | "createdTime": 1550383020, 99 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 100 | }, 101 | "assetImmutableData": { 102 | "immutableSchemaUrl": undefined, 103 | "immutableSchemaType": undefined, 104 | "immutableUrl": undefined 105 | }, 106 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 107 | "assetOwnership": { 108 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 109 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 110 | ownershipHistoryRecords: [] 111 | } 112 | } 113 | }); 114 | }); 115 | 116 | it('should return latest state of txid updated and validation', async () => { 117 | var result = await index.getClient().find('4b541e091d6af2f60d256dfca2685f95bf2d3a9b7995595115183d37b7c5bf90', true); 118 | expect(result).to.eql({ 119 | success: true, 120 | data: { 121 | "txid": "4b541e091d6af2f60d256dfca2685f95bf2d3a9b7995595115183d37b7c5bf90", 122 | "assetDataCurrent": { 123 | dataUrl: 'b://9a201f03464ef345ae7d8fd49bdf09eddc52a96345358438af4f13af7f4b56fa', 124 | dataSchemaUrl: 'b://d3df63599f8e1d02d98f263f1a0427de01c95f077758c58f2d7a6bd94ea6da70', 125 | dataSchemaType: 'http://json-schema.org/draft-07/schema#', 126 | "metadataSchemaUrl": undefined, 127 | "metadataSchemaType": undefined, 128 | "metadataUrl": undefined, 129 | }, 130 | "assetDataOriginal": { 131 | dataUrl: 'b://9a201f03464ef345ae7d8fd49bdf09eddc52a96345358438af4f13af7f4b56fa', 132 | dataSchemaUrl: 'b://d3df63599f8e1d02d98f263f1a0427de01c95f077758c58f2d7a6bd94ea6da70', 133 | dataSchemaType: 'http://json-schema.org/draft-07/schema#', 134 | "metadataSchemaUrl": undefined, 135 | "metadataSchemaType": undefined, 136 | "metadataUrl": undefined, 137 | }, 138 | "blockInfoCurrent": { 139 | "blockHeight": 570305, 140 | "createdTime": 1550546281, 141 | "txid": "4b541e091d6af2f60d256dfca2685f95bf2d3a9b7995595115183d37b7c5bf90", 142 | }, 143 | "blockInfoOriginal": { 144 | "blockHeight": 570305, 145 | "createdTime": 1550546281, 146 | "txid": "4b541e091d6af2f60d256dfca2685f95bf2d3a9b7995595115183d37b7c5bf90", 147 | }, 148 | "assetImmutableData": { 149 | "immutableSchemaUrl": undefined, 150 | "immutableSchemaType": undefined, 151 | "immutableUrl": undefined 152 | }, 153 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 154 | "assetOwnership": { 155 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 156 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 157 | ownershipHistoryRecords: [] 158 | } 159 | } 160 | }); 161 | }); 162 | }); -------------------------------------------------------------------------------- /test/getAssetMintedVersion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var index = require('../dist/index.js'); 4 | 5 | describe('getAssetMintedVersion function test', () => { 6 | it('should return false no txid', async () => { 7 | var result = await index.getClient().getAssetMintedVersion(); 8 | expect(result).to.eql({ 9 | success: false, 10 | message: "txid required" 11 | }); 12 | }); 13 | 14 | it('should return false txid not found', async () => { 15 | var result = await index.getClient().getAssetMintedVersion('0000276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c'); 16 | expect(result).to.eql({ 17 | success: false, 18 | message: "tx not found" 19 | }); 20 | }); 21 | 22 | it('should return true txid found', async () => { 23 | var result = await index.getClient().getAssetMintedVersion('364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa'); 24 | expect(result).to.eql({ 25 | success: true, 26 | data: { 27 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 28 | "assetDataCurrent": { 29 | "dataSchemaUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#matter/schema/post/draft-01", 30 | "dataSchemaType": "http://json-schema.org/draft-07/schema#", 31 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 32 | "metadataSchemaUrl": undefined, 33 | "metadataSchemaType": undefined, 34 | "metadataUrl": undefined, 35 | }, 36 | "assetDataOriginal": { 37 | "dataSchemaUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#matter/schema/post/draft-01", 38 | "dataSchemaType": "http://json-schema.org/draft-07/schema#", 39 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 40 | "metadataSchemaUrl": undefined, 41 | "metadataSchemaType": undefined, 42 | "metadataUrl": undefined, 43 | }, 44 | "blockInfoCurrent": { 45 | "blockHeight": 570023, 46 | "createdTime": 1550383020, 47 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 48 | }, 49 | "blockInfoOriginal": { 50 | "blockHeight": 570023, 51 | "createdTime": 1550383020, 52 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 53 | }, 54 | "assetImmutableData": { 55 | "immutableSchemaUrl": undefined, 56 | "immutableSchemaType": undefined, 57 | "immutableUrl": undefined 58 | }, 59 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 60 | "assetOwnership": { 61 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 62 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 63 | ownershipHistoryRecords: [] 64 | } 65 | } 66 | }); 67 | }); 68 | }); -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var index = require('../dist/index.js'); 4 | 5 | const privateKey = 'your key'; 6 | 7 | describe('integration test test', () => { 8 | 9 | const sleep = async (timeout = 4000) => { 10 | return await new Promise((resolve) => { 11 | setTimeout(() => { 12 | resolve(); 13 | }, timeout); 14 | }); 15 | } 16 | 17 | it('should create, update, and retrieve', async () => { 18 | // console.log('integration_test: starting'); 19 | var createRequest = { 20 | asset: { 21 | dataUrl: 'b://2e6e7d0be54925cd0a841d3f514308a1b02ed79242bb1c040350d07f563f0ca7', 22 | updateAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz' 23 | }, 24 | pay: { 25 | key: privateKey 26 | } 27 | }; 28 | var result = await index.getClient().create(createRequest); 29 | expect(result.success).to.equal(true); 30 | expect(result.data.txid).to.not.be.null; 31 | console.log('created asset', result); 32 | // console.log('integration_test: asset created', result); 33 | await sleep(4000); 34 | 35 | var found = await index.getClient().find(result.data.txid); 36 | expect(found.success).to.equal(true); 37 | expect(found.data.txid).to.not.be.null; 38 | // console.log('integration_test: asset found', found); 39 | 40 | var update = await index.getClient().update({ 41 | txid: found.data.txid, 42 | update: Object.assign({}, found.data.assetDataCurrent, 43 | { 44 | dataUrl: 'b://df356acebad6642bf7859d2de48eb3d6e3917d9cd360dcb8eea02ca7d7602206' 45 | } 46 | ), 47 | pay: { 48 | key: privateKey 49 | } 50 | }); 51 | // console.log('integration_test: asset update', update); 52 | expect(update.success).to.equal(true); 53 | expect(update.data.txid).to.not.be.null; 54 | 55 | await sleep(4000); 56 | 57 | var foundUpdated = await index.getClient().find(found.data.txid); 58 | // console.log('found updated', foundUpdated); 59 | expect(foundUpdated.success).to.equal(true); 60 | expect(foundUpdated.data.txid).to.not.be.null; 61 | 62 | expect(foundUpdated.data.assetDataOriginal.dataUrl).to.equal('data'); 63 | expect(foundUpdated.data.assetDataOriginal.metadataUrl).to.equal('metadata'); 64 | 65 | expect(foundUpdated.data.assetDataCurrent.dataUrl).to.equal('data'); 66 | expect(foundUpdated.data.assetDataCurrent.metadataUrl).to.equal('metadata'); 67 | expect(foundUpdated.data.assetDataCurrent.metadataSchemaUrl).to.equal('metadataSchemaUrl'); 68 | 69 | }); 70 | 71 | }); -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 60000 2 | 3 | -------------------------------------------------------------------------------- /test/update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var index = require('../dist/index.js'); 4 | 5 | const privateKey = 'wifkeyhere'; 6 | 7 | describe('update function test', () => { 8 | it('should return false no data', async () => { 9 | var result = await index.getClient().update(); 10 | expect(result).to.eql({ 11 | success: false, 12 | message: "key required" 13 | }); 14 | }); 15 | 16 | it('should update asset fail', async () => { 17 | var result = await index.getClient().update({ 18 | update: { 19 | dataUrl: undefined, 20 | }, 21 | pay: { 22 | key: 'key1' 23 | } 24 | }); 25 | expect(result).to.eql({ 26 | success: false, 27 | message: 'txid required' 28 | }); 29 | }); 30 | 31 | it('should update asset failure', async () => { 32 | 33 | var result = await index.getClient().update({ 34 | txid: '364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa', 35 | update: { 36 | dataUrl: undefined, 37 | dataSchemaUrl: undefined, 38 | dataSchemaType: undefined 39 | }, 40 | pay: { 41 | key: 'key1' 42 | } 43 | }); 44 | expect(result).to.eql({ 45 | success: false, 46 | message: 'update.dataUrl required or update.dataSchemaUrl required or update.dataSchemaType update.metadataUrl required or update.metadataSchemaUrl required or update.metadataSchemaType required' 47 | }); 48 | }); 49 | 50 | /* 51 | it('should update asset success', async () => { 52 | const result = await index.getClient().update({ 53 | txid: '172908fd19df177356e369867623bb1f9d90b8d1d55c384ebd468985d7da035e', 54 | update: { 55 | dataUrl: 'b://updated', 56 | dataSchemaUrl: 'b://updateddescription', 57 | dataSchemaType: 'b://dataSchemaType', 58 | }, 59 | pay: { 60 | key: privateKey 61 | } 62 | }); 63 | console.log('result', result); 64 | expect(result.data.txid).to.not.be.null; 65 | });*/ 66 | }); -------------------------------------------------------------------------------- /test/validateAssetStructure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var index = require('../dist/index.js'); 4 | 5 | describe('validateAssetStructure function test', () => { 6 | it('validate success', async () => { 7 | var asset = { 8 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 9 | "assetDataCurrent": { 10 | "dataSchemaUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#matter/schema/post/draft-01", 11 | "dataSchemaType": "http://json-schema.org/draft-07/schema#", 12 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 13 | }, 14 | "assetDataOriginal": { 15 | "dataSchemaUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c#matter/schema/post/draft-01", 16 | "dataSchemaType": "http://json-schema.org/draft-07/schema#", 17 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c" 18 | }, 19 | "blockInfoCurrent": { 20 | "blockHeight": 570023, 21 | "createdTime": 1550383020, 22 | }, 23 | "blockInfoOriginal": { 24 | "blockHeight": 570023, 25 | "createdTime": 1550383020, 26 | }, 27 | assetImmutableData: undefined, 28 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 29 | "assetOwnership": { 30 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 31 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 32 | ownershipHistoryRecords: [] 33 | } 34 | }; 35 | 36 | var result = index.getBitcoinAsset().validateAssetStructure(asset); 37 | expect(result).to.eql(asset); 38 | }); 39 | 40 | it('validate minimal success', async () => { 41 | var asset = { 42 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 43 | "assetDataCurrent": { 44 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 45 | }, 46 | "assetDataOriginal": { 47 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c" 48 | }, 49 | "blockInfoCurrent": { 50 | "blockHeight": undefined, 51 | "createdTime": undefined, 52 | }, 53 | "blockInfoOriginal": { 54 | "blockHeight": undefined, 55 | "createdTime": undefined, 56 | }, 57 | assetImmutableData: undefined, 58 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 59 | "assetOwnership": { 60 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 61 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 62 | ownershipHistoryRecords: [] 63 | } 64 | }; 65 | 66 | var result = index.getBitcoinAsset().validateAssetStructure(asset); 67 | expect(result).to.eql(asset); 68 | }); 69 | 70 | it('validate less than minimal failure', async () => { 71 | var asset = { 72 | "assetDataCurrent": { 73 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 74 | }, 75 | "assetDataOriginal": { 76 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c" 77 | }, 78 | "blockInfoCurrent": { 79 | "blockHeight": undefined, 80 | "createdTime": undefined, 81 | }, 82 | "blockInfoOriginal": { 83 | "blockHeight": undefined, 84 | "createdTime": undefined, 85 | }, 86 | assetImmutableData: undefined, 87 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 88 | "assetOwnership": { 89 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 90 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 91 | ownershipHistoryRecords: [] 92 | } 93 | }; 94 | expect(function(){ 95 | index.getBitcoinAsset().validateAssetStructure(asset) 96 | }).to.throw('undefined txid'); 97 | 98 | var asset = { 99 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 100 | "assetDataCurrent": { 101 | }, 102 | "assetDataOriginal": { 103 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c" 104 | }, 105 | "blockInfoCurrent": { 106 | "blockHeight": undefined, 107 | "createdTime": undefined, 108 | }, 109 | "blockInfoOriginal": { 110 | "blockHeight": undefined, 111 | "createdTime": undefined, 112 | }, 113 | assetImmutableData: undefined, 114 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 115 | "assetOwnership": { 116 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 117 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 118 | ownershipHistoryRecords: [] 119 | } 120 | }; 121 | expect(function(){ 122 | index.getBitcoinAsset().validateAssetStructure(asset) 123 | }).to.throw('undefined assetDataCurrent.dataUrl'); 124 | 125 | var asset = { 126 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 127 | "assetDataCurrent": { 128 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 129 | }, 130 | "assetDataOriginal": { 131 | }, 132 | "blockInfoCurrent": { 133 | "blockHeight": undefined, 134 | "createdTime": undefined, 135 | }, 136 | "blockInfoOriginal": { 137 | "blockHeight": undefined, 138 | "createdTime": undefined, 139 | }, 140 | assetImmutableData: undefined, 141 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 142 | "assetOwnership": { 143 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 144 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 145 | ownershipHistoryRecords: [] 146 | } 147 | }; 148 | expect(function(){ 149 | index.getBitcoinAsset().validateAssetStructure(asset) 150 | }).to.throw('undefined assetDataOriginal.dataUrl'); 151 | 152 | var asset = { 153 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 154 | "assetDataCurrent": { 155 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 156 | }, 157 | "assetDataOriginal": { 158 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c" 159 | }, 160 | "blockInfoCurrent": { 161 | "blockHeight": undefined, 162 | "createdTime": undefined, 163 | }, 164 | "blockInfoOriginal": { 165 | "blockHeight": undefined, 166 | "createdTime": undefined, 167 | }, 168 | assetImmutableData: undefined, 169 | "assetOwnership": { 170 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 171 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 172 | ownershipHistoryRecords: [] 173 | } 174 | }; 175 | 176 | expect(function(){ 177 | index.getBitcoinAsset().validateAssetStructure(asset) 178 | }).to.throw('undefined updateAddress'); 179 | 180 | var asset = { 181 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 182 | "assetDataCurrent": { 183 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 184 | }, 185 | "assetDataOriginal": { 186 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c" 187 | }, 188 | "blockInfoCurrent": { 189 | "blockHeight": undefined, 190 | "createdTime": undefined, 191 | }, 192 | "blockInfoOriginal": { 193 | "blockHeight": undefined, 194 | "createdTime": undefined, 195 | }, 196 | assetImmutableData: undefined, 197 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 198 | "assetOwnership": { 199 | currentOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 200 | ownershipHistoryRecords: [] 201 | } 202 | }; 203 | 204 | expect(function(){ 205 | index.getBitcoinAsset().validateAssetStructure(asset) 206 | }).to.throw('assetOwnership.originalOwnerAddress'); 207 | 208 | var asset = { 209 | "txid": "364aeca04e251328470bcebb7d3a877944c6c39b5250f17cbeece88af459fdfa", 210 | "assetDataCurrent": { 211 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c", 212 | }, 213 | "assetDataOriginal": { 214 | "dataUrl": "b://3150276948348c428d2f86596953d503a4e8508b2238201a3b7e281b180d7c4c" 215 | }, 216 | "blockInfoCurrent": { 217 | "blockHeight": undefined, 218 | "createdTime": undefined, 219 | }, 220 | "blockInfoOriginal": { 221 | "blockHeight": undefined, 222 | "createdTime": undefined, 223 | }, 224 | assetImmutableData: undefined, 225 | "updateAddress": "1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz", 226 | "assetOwnership": { 227 | originalOwnerAddress: '1EXhSbGFiEAZCE5eeBvUxT6cBVHhrpPWXz', 228 | ownershipHistoryRecords: [] 229 | } 230 | }; 231 | 232 | expect(function(){ 233 | index.getBitcoinAsset().validateAssetStructure(asset) 234 | }).to.throw('assetOwnership.currentOwnerAddress'); 235 | }); 236 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "noImplicitAny": false 9 | } 10 | } 11 | 12 | --------------------------------------------------------------------------------