├── .babelrc ├── .gitignore ├── .prettierrc ├── README.md ├── index.html ├── package.json ├── rollup.config.js ├── src ├── main.js ├── payload-builder.js └── payloads.js ├── test ├── client.js ├── node.js └── token_bg.wasm ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "usage", 7 | "corejs": 3 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.idea 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `wavelet-client` 2 | 3 | [![crates.io](https://img.shields.io/npm/v/wavelet-client.svg)](https://www.npmjs.com/package/wavelet-client) 4 | [![Discord Chat](https://img.shields.io/discord/458332417909063682.svg)](https://discord.gg/dMYfDPM) 5 | 6 | A developer-friendly stateless HTTP client for interacting with a Wavelet node. Wrriten in JavaScript. 7 | 8 | 9 | ### **Wavelet (Himitsu)** 10 | Starting from v2, **wavelet-client** will support the new version of Wavelet (Himitsu). 11 | 12 | For support of older Wavelet please use v1. 13 | 14 | The entire source code of this client was written to just fit within a single JavaScript file to make 15 | the underlying code simple and easy to understand. The client has a _very_ minimal set of dependencies 16 | that are well-audited. 17 | 18 | The client has been tested to work on both NodeJS alongside on the browser. As a warning, the client uses 19 | some newer language features such as big integers which may require a polyfill. 20 | 21 | ## Setup 22 | 23 | ```shell 24 | yarn add wavelet-client 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```javascript 30 | const {Wavelet, Contract, TAG_TRANSFER, JSBI} = require('wavelet-client'); 31 | 32 | const BigInt = JSBI.BigInt; 33 | 34 | const client = new Wavelet("http://127.0.0.1:9000"); 35 | 36 | (async () => { 37 | console.log(Wavelet.generateNewWallet()); 38 | console.log(await client.getNodeInfo()); 39 | 40 | console.log(await client.getAccount('400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405')); 41 | 42 | const transfer = await client.getTransaction('805e4ff2a9955b804e32579166c8a54e07e3f1c161702254d8778e4805ea12fc'); 43 | console.log(Wavelet.parseTransaction(transfer.tag, transfer.payload)); 44 | 45 | const call = await client.getTransaction('9a8746b7bf7a84af7fbd41520a841e96907bee71a88560af7e6996cfb7682891'); 46 | console.log(Wavelet.parseTransaction(call.tag, call.payload)); 47 | 48 | const stake = await client.getTransaction('673ef140f8a47980d8684a47bf639624d7a4d8470ad30c1a66a4f417f69ab84a'); 49 | console.log(Wavelet.parseTransaction(stake.tag, stake.payload)); 50 | 51 | const wallet = Wavelet.loadWalletFromPrivateKey('87a6813c3b4cf534b6ae82db9b1409fa7dbd5c13dba5858970b56084c4a930eb400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405'); 52 | const account = await client.getAccount(Buffer.from(wallet.publicKey).toString("hex")); 53 | 54 | const contract = new Contract(client, '52bb52e0440ce0aa7a7d2018f5bac21d6abde64f5b9498615ce2bef332bd487a'); 55 | await contract.init(); 56 | 57 | console.log(contract.test(wallet, 'balance', BigInt(0), 58 | { 59 | type: 'raw', 60 | value: '400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405' 61 | }, 62 | )); 63 | 64 | console.log(await contract.call(wallet, 'balance', BigInt(0), BigInt(0), JSBI.subtract(BigInt(account.balance), BigInt(1000000)), 65 | { 66 | type: 'raw', 67 | value: '400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405' 68 | }, 69 | )); 70 | 71 | const consensusPoll = await client.pollConsensus({onRoundEnded: console.log}); 72 | const transactionsPoll = await client.pollTransactions({onTransactionApplied: console.log}, {tag: TAG_TRANSFER, creator: "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405"}); 73 | const accountsPoll = await client.pollAccounts({onAccountUpdated: console.log}, {id: "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405"}); 74 | 75 | for (let i = 0; i < 100; i++) { 76 | await client.transfer(wallet, 'e49e8be205a00edb45de8183a4374e362efc9a4da56dd7ba17e2dd780501e49f', BigInt(1000000)); 77 | } 78 | })(); 79 | ``` -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | UMD Test 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wavelet-client", 3 | "version": "2.0.0-rc.6", 4 | "main": "dist/wavelet-client.cjs.js", 5 | "module": "dist/wavelet-client.esm.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rollup -c && npm run build:umd", 9 | "build:umd": "webpack", 10 | "dev:umd": "webpack -w", 11 | "dev": "rollup -c -w", 12 | "test": "rollup -c && node test/node.js", 13 | "test:umd": "http-server .", 14 | "prepublish": "npm run build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/perlin-network/wavelet-client-js.git" 19 | }, 20 | "keywords": [ 21 | "smart-contracts", 22 | "wasm", 23 | "utils", 24 | "lib", 25 | "wavelet", 26 | "perlin" 27 | ], 28 | "dependencies": { 29 | "atob": "^2.1.2", 30 | "axios": "^0.19.0", 31 | "bigint-buffer": "^1.1.5", 32 | "blakejs": "^1.1.0", 33 | "core-js": "^3.3.3", 34 | "jsbi": "^3.1.1", 35 | "json-bigint": "^0.3.0", 36 | "text-encoding": "^0.7.0", 37 | "tweetnacl": "^1.0.1", 38 | "url": "^0.11.0", 39 | "websocket": "^1.0.28" 40 | }, 41 | "files": [ 42 | "dist" 43 | ], 44 | "devDependencies": { 45 | "@babel/core": "^7.6.4", 46 | "@babel/preset-env": "^7.6.3", 47 | "@babel/runtime": "^7.5.5", 48 | "babel-loader": "^8.0.6", 49 | "http-server": "^0.12.0", 50 | "rollup": "^1.16.3", 51 | "webpack": "^4.39.1", 52 | "webpack-cli": "^3.3.6" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | 3 | export default [ 4 | { 5 | input: 'src/main.js', 6 | output: [ 7 | {file: pkg.main, format: 'cjs'}, 8 | {file: pkg.module, format: 'es'} 9 | ] 10 | } 11 | ] -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import atob from "atob"; 3 | import nacl from "tweetnacl"; 4 | import url from "url"; 5 | import { Buffer } from "buffer"; 6 | import { blake2b } from "blakejs"; 7 | import * as payloads from "./payloads"; 8 | import JSBI from "jsbi"; 9 | import JSONbig from "json-bigint"; 10 | import WebSocket from "websocket"; 11 | const WebSocketClient = WebSocket.w3cwebsocket; 12 | 13 | const TAG_TRANSFER = 1; 14 | const TAG_CONTRACT = 2; 15 | const TAG_STAKE = 3; 16 | const TAG_BATCH = 4; 17 | 18 | if (typeof window === 'undefined') { 19 | var window = window || {}; 20 | var global = global || window; 21 | } 22 | 23 | /** 24 | * Converts a string to a Buffer. 25 | * 26 | * @param {string} str 27 | * @returns {ArrayBuffer} 28 | */ 29 | const str2ab = str => { 30 | const buf = new ArrayBuffer(str.length); 31 | const view = new Uint8Array(buf); 32 | for (var i = 0, len = str.length; i < len; i++) { 33 | view[i] = str.charCodeAt(i); 34 | } 35 | return buf; 36 | }; 37 | 38 | DataView.prototype._setBigUint64 = DataView.prototype.setBigUint64; 39 | DataView.prototype.setBigUint64 = function (byteOffset, value, littleEndian) { 40 | if (typeof value === 'bigint' && typeof this._setBigUint64 !== 'undefined') { 41 | this._setBigUint64(byteOffset, value, littleEndian); 42 | } else if (value.constructor === JSBI && typeof value.sign === 'bigint' && typeof this._setBigUint64 !== 'undefined') { 43 | this._setBigUint64(byteOffset, value.sign, littleEndian); 44 | } else if (value.constructor === JSBI || (value.constructor && typeof value.constructor.BigInt === 'function')) { 45 | let lowWord = value[0], highWord = value.length >= 2 ? value[1] : 0; 46 | 47 | this.setUint32(littleEndian ? byteOffset : byteOffset + 4, lowWord, littleEndian); 48 | this.setUint32(littleEndian ? byteOffset + 4 : byteOffset, highWord, littleEndian); 49 | } else { 50 | throw TypeError('Value needs to be BigInt or JSBI'); 51 | } 52 | } 53 | 54 | DataView.prototype._getBigUint64 = DataView.prototype.getBigUint64; 55 | DataView.prototype.getBigUint64 = function (byteOffset, littleEndian) { 56 | if (typeof this._getBigUint64 !== 'undefined' && window.useNativeBigIntsIfAvailable) { 57 | return this._getBigUint64(byteOffset, littleEndian); 58 | } else { 59 | let lowWord = this.getUint32(littleEndian ? byteOffset : byteOffset + 4, littleEndian); 60 | let highWord = this.getUint32(littleEndian ? byteOffset + 4 : byteOffset, littleEndian); 61 | 62 | const result = new JSBI(2, false); 63 | result.__setDigit(0, lowWord); 64 | result.__setDigit(1, highWord); 65 | return result; 66 | } 67 | } 68 | 69 | if (!global.TextDecoder) { 70 | global.TextDecoder = require("text-encoding").TextDecoder; 71 | } 72 | 73 | if (!ArrayBuffer.transfer) { // Polyfill just in-case. 74 | /** 75 | * The static ArrayBuffer.transfer() method returns a new ArrayBuffer whose contents have 76 | * been taken from the oldBuffer's data and then is either truncated or zero-extended by 77 | * newByteLength. If newByteLength is undefined, the byteLength of the oldBuffer is used. 78 | * 79 | * This operation leaves oldBuffer in a detached state. 80 | * 81 | * @param {Uint8Array} oldBuffer 82 | * @param {number} newByteLength 83 | * @returns {ArrayBufferLike} 84 | */ 85 | ArrayBuffer.transfer = (oldBuffer, newByteLength) => { 86 | if (!(oldBuffer instanceof ArrayBuffer)) 87 | throw new TypeError('Source must be an instance of ArrayBuffer'); 88 | 89 | if (newByteLength <= oldBuffer.byteLength) 90 | return oldBuffer.slice(0, newByteLength); 91 | 92 | const destView = new Uint8Array(new ArrayBuffer(newByteLength)); 93 | destView.set(new Uint8Array(oldBuffer)); 94 | 95 | return destView.buffer; 96 | }; 97 | } 98 | 99 | 100 | 101 | class Contract { 102 | /** 103 | * A Wavelet smart contract execution simulator. 104 | * 105 | * @param {Wavelet} client Client instance which is connected to a single Wavelet node. 106 | * @param {string} contract_id Hex-encoded ID of a smart contract. 107 | */ 108 | constructor(client, contract_id) { 109 | this.client = client; 110 | this.contract_id = contract_id; 111 | 112 | this.contract_payload = { 113 | round_idx: JSBI.BigInt(0), 114 | round_id: "0000000000000000000000000000000000000000000000000000000000000000", 115 | transaction_id: "0000000000000000000000000000000000000000000000000000000000000000", 116 | sender_id: "0000000000000000000000000000000000000000000000000000000000000000", 117 | amount: JSBI.BigInt(0), 118 | params: new Uint8Array(new ArrayBuffer(0)), 119 | }; 120 | 121 | this.decoder = new global.TextDecoder(); 122 | 123 | this.result = null; 124 | this.logs = []; 125 | 126 | this.contract_payload_buf = payloads.rebuildContractPayload(this.contract_payload); 127 | } 128 | 129 | /** 130 | * Sets the consensus round index for all future simulated smart contract calls. 131 | * 132 | * @param {bigint} round_idx Consensus round index. 133 | */ 134 | setRoundIndex(round_idx) { 135 | this.contract_payload.round_idx = round_idx; 136 | } 137 | 138 | /** 139 | * Sets the consensus round ID for all future simulated smart contract calls. 140 | * 141 | * @param {string} round_id A 64-letter hex-encoded consensus round ID. 142 | */ 143 | setRoundID(round_id) { 144 | if (round_id.length !== 64) throw new Error("round id must be 64 letters and hex-encoded"); 145 | this.contract_payload.round_id = round_id; 146 | } 147 | 148 | /** 149 | * Sets the ID of the transaction used to make all future simulated smart contract calls. 150 | * 151 | * @param {string} transaction_id A 64-letter ex-encoded transaction ID. 152 | */ 153 | setTransactionID(transaction_id) { 154 | if (transaction_id.length !== 64) throw new Error("transaction id must be 64 letters and hex-encoded"); 155 | this.contract_payload.transaction_id = transaction_id; 156 | } 157 | 158 | /** 159 | * Sets the sender ID for all future simulated smart contract calls. 160 | * 161 | * @param {string} sender_id A 64-letter hex-encoded sender wallet address ID. 162 | */ 163 | setSenderID(sender_id) { 164 | if (sender_id.length !== 64) throw new Error("sender id must be 64 letters and hex-encoded"); 165 | this.contract_payload.sender_id = sender_id; 166 | } 167 | 168 | /** 169 | * Simulates a call to the smart contract. init() must be called to initialize the WebAssembly VM 170 | * before calls may be performed against this specified smart contract. 171 | * 172 | * @param {string} func_name Name of the smart contract function to call. 173 | * @param {bigint} amount_to_send Amount of PERLs to send simultaneously to the smart contract 174 | * while calling a function. 175 | * @param {...{type: ('int16'|'int32'|'int64'|'uint16'|'uint32'|'uint64'|'byte'|'raw'|'bytes'|'string'), value: number|string|ArrayBuffer|Uint8Array}} func_params Variadic list of arguments. 176 | * @returns {{result: string|undefined, logs: Array}} 177 | */ 178 | test(wallet, func_name, amount_to_send, ...func_params) { 179 | if (this.vm === undefined) throw new Error("init() needs to be called before calling test()"); 180 | 181 | func_name = "_contract_" + func_name; 182 | 183 | if (!(func_name in this.vm.instance.exports)) { 184 | throw new Error("could not find function in smart contract"); 185 | } 186 | 187 | this.contract_payload.params = payloads.parseFunctionParams(...func_params); 188 | this.contract_payload.amount = payloads.normalizeNumber(amount_to_send); 189 | this.contract_payload.sender_id = Buffer.from(wallet.publicKey).toString("hex"); 190 | this.contract_payload_buf = payloads.rebuildContractPayload(this.contract_payload); 191 | 192 | // Clone the current browser VM's memory. 193 | const copy = ArrayBuffer.transfer(this.vm.instance.exports.memory.buffer, this.vm.instance.exports.memory.buffer.byteLength); 194 | 195 | // Call the function. 196 | this.vm.instance.exports[func_name](); 197 | 198 | // Collect simulated execution results. 199 | const res = {result: this.result, logs: this.logs}; 200 | 201 | // Reset the browser VM. 202 | new Uint8Array(this.vm.instance.exports.memory.buffer, 0, copy.byteLength).set(copy); 203 | 204 | // Reset all func_params and results and logs. 205 | this.contract_payload.params = new Uint8Array(new ArrayBuffer(0)); 206 | this.result = null; 207 | this.logs = []; 208 | 209 | return res; 210 | } 211 | 212 | /** 213 | * Performs an official call to a specified smart contract function with a provided gas limit, and a variadic list 214 | * of arguments under a provided Wavelet wallet instance. 215 | * 216 | * @param wallet Wavelet wallet. 217 | * @param func_name Name of the smart contract function to call. 218 | * @param amount_to_send Amount of PERLs to send simultaneously to the smart contract while 219 | * calling a function. 220 | * @param gas_limit Gas limit to expend for invoking a smart contract function. 221 | * @param gas_deposit Amount of gas fees to deposit into the smart contract. 222 | * @param {...{type: ('int16'|'int32'|'int64'|'uint16'|'uint32'|'uint64'|'byte'|'raw'|'bytes'|'string'), value: number|string|ArrayBuffer|Uint8Array}} func_params Variadic list of arguments. 223 | * @returns {Promise} Response from the Wavelet node. 224 | */ 225 | async call(wallet, func_name, amount_to_send, gas_limit, gas_deposit, ...func_params) { 226 | return await this.client.transfer(wallet, this.contract_id, amount_to_send, gas_limit, gas_deposit, func_name, payloads.parseFunctionParams(...func_params)); 227 | } 228 | 229 | 230 | 231 | 232 | 233 | /** 234 | * Fetches and re-loads the memory of the backing WebAssembly VM for this smart contract; optionally 235 | * growing the number of memory pages associated to the VM should there be not enough memory to hold 236 | * any new updates to the smart contracts memory. init() must be called before this function may be 237 | * called. 238 | * 239 | * @returns {Promise} 240 | */ 241 | async fetchAndPopulateMemoryPages() { 242 | if (this.vm === undefined) throw new Error("init() needs to be called before calling fetchAndPopulateMemoryPages()"); 243 | 244 | const account = await this.client.getAccount(this.contract_id); 245 | const loaded_memory = await this.client.getMemoryPages(account.public_key, account.num_mem_pages); 246 | 247 | const num_mem_pages = this.vm.instance.exports.memory.buffer.byteLength / 65536; 248 | const num_loaded_mem_pages = loaded_memory.byteLength / 65536; 249 | if (num_mem_pages < num_loaded_mem_pages) { 250 | this.vm.instance.exports.memory.grow(num_loaded_mem_pages - num_mem_pages); 251 | } 252 | 253 | new Uint8Array(this.vm.instance.exports.memory.buffer, 0, loaded_memory.byteLength).set(loaded_memory); 254 | } 255 | 256 | /** 257 | * Downloads smart contract code from the Wavelet node if available, and initializes 258 | * a WebAssembly VM to simulate function calls against the contract. 259 | * 260 | * @returns {Promise} 261 | */ 262 | async init() { 263 | this.code = await this.client.getCode(this.contract_id); 264 | 265 | const imports = { 266 | env: { 267 | abort: () => { 268 | }, 269 | _send_transaction: (tag, payload_ptr, payload_len) => { 270 | const payload_view = new Uint8Array(this.vm.instance.exports.memory.buffer, payload_ptr, payload_len); 271 | const payload = this.decoder.decode(payload_view); 272 | console.log(`Sent transaction with tag ${tag} and payload ${params}.`); 273 | }, 274 | _payload_len: () => { 275 | return this.contract_payload_buf.byteLength; 276 | }, 277 | _payload: payload_ptr => { 278 | const view = new Uint8Array(this.vm.instance.exports.memory.buffer, payload_ptr, this.contract_payload_buf.byteLength); 279 | view.set(this.contract_payload_buf); 280 | }, 281 | _result: (ptr, len) => { 282 | this.result = this.decoder.decode(new Uint8Array(this.vm.instance.exports.memory.buffer, ptr, len)); 283 | }, 284 | _log: (ptr, len) => { 285 | const view = new Uint8Array(this.vm.instance.exports.memory.buffer, ptr, len); 286 | this.logs.push(this.decoder.decode(view)); 287 | }, 288 | _verify_ed25519: () => { 289 | }, 290 | _hash_blake2b_256: () => { 291 | }, 292 | _hash_sha256: () => { 293 | }, 294 | _hash_sha512: () => { 295 | }, 296 | } 297 | }; 298 | 299 | this.vm = await WebAssembly.instantiate(this.code, imports); 300 | await this.fetchAndPopulateMemoryPages(); 301 | } 302 | } 303 | 304 | class Wavelet { 305 | /** 306 | * A client for interacting with the HTTP API of a Wavelet node. 307 | * 308 | * @param {string} host Address to the HTTP API of a Wavelet node. 309 | * @param {Object=} opts Default options to be passed for making any HTTP request calls using this client instance (optional). 310 | */ 311 | constructor(host, opts = {}, useMoonlet = false) { 312 | this.host = host; 313 | this.initLastBlock(); 314 | 315 | this.opts = { 316 | ...opts, 317 | transformRequest: [(data, headers) => { 318 | headers.common = {}; 319 | 320 | return data; 321 | }], 322 | transformResponse: [(data) => { 323 | if (typeof data !== 'string') return data; 324 | return JSONbig.parse(data); 325 | }] 326 | }; 327 | } 328 | 329 | 330 | async initLastBlock() { 331 | const { block } = await this.getNodeInfo(); 332 | this.lastBlock = block.height; 333 | } 334 | /** 335 | * Query for information about the node you are connected to. 336 | * 337 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 338 | * @returns {Promise} 339 | */ 340 | async getNodeInfo(opts) { 341 | return (await axios.get(`${this.host}/ledger`, {...this.opts, ...opts})).data; 342 | } 343 | 344 | /** 345 | * Query for details of a transaction. 346 | * 347 | * @param {string} id Hex-encoded transaction ID. 348 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 349 | * @returns {Promise} 350 | */ 351 | async getTransaction(id, opts = {}) { 352 | return (await axios.get(`${this.host}/tx/${id}`, {...this.opts, ...opts})).data; 353 | } 354 | 355 | /** 356 | * Query for details of an account; whether it be a smart contract or a user. 357 | * 358 | * @param {string} id Hex-encoded account/smart contract address. 359 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 360 | * @returns {Promise<{public_key: string, nonce: bigint, balance: bigint, stake: bigint, reward: bigint, is_contract: boolean, num_mem_pages: bigint}>} 361 | */ 362 | async getAccount(id, opts = {}) { 363 | const response = await axios.get(`${this.host}/accounts/${id}`, {...this.opts, ...opts}); 364 | return response.data; 365 | } 366 | 367 | /** 368 | * Query for the raw WebAssembly code of a smart contract. 369 | * 370 | * @param string} id Hex-encoded ID of the smart contract. 371 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 372 | * @returns {Promise} 373 | */ 374 | async getCode(id, opts = {}) { 375 | return new Uint8Array((await axios.get(`${this.host}/contract/${id}`, { 376 | ...this.opts, ...opts, 377 | responseType: 'arraybuffer', 378 | responseEncoding: 'binary' 379 | })).data); 380 | } 381 | 382 | /** 383 | * Query for the amalgamated WebAssembly VM memory of a given smart contract. 384 | * 385 | * @param {string} id Hex-encoded ID of the smart contract. 386 | * @param {number} num_mem_pages Number of memory pages the smart contract has. 387 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 388 | * @returns {Promise} The memory of the given smart contract, which may be used to 389 | * initialize a WebAssembly VM with (either on browser/desktop). 390 | */ 391 | async getMemoryPages(id, num_mem_pages, opts = {}) { 392 | if (num_mem_pages === 0) throw new Error("num pages cannot be zero"); 393 | 394 | const memory = new Uint8Array(new ArrayBuffer(65536 * num_mem_pages)); 395 | const reqs = []; 396 | 397 | for (let idx = 0; idx < num_mem_pages; idx++) { 398 | reqs.push((async () => { 399 | try { 400 | const res = await axios.get(`${this.host}/contract/${id}/page/${idx}`, { 401 | ...this.opts, ...opts, 402 | responseType: 'arraybuffer', 403 | responseEncoding: 'binary' 404 | }); 405 | 406 | if (res.status === 200) { 407 | const page = new Uint8Array(res.data); 408 | memory.set(page, 65536 * idx); 409 | } 410 | } catch (error) { 411 | } 412 | })()); 413 | } 414 | 415 | await Promise.all(reqs); 416 | 417 | return memory; 418 | } 419 | 420 | /** 421 | * Transfer some amount of PERLs to a recipient, or invoke a function on 422 | * a smart contract should the recipient specified be a smart contract. 423 | * 424 | * @param {nacl.SignKeyPair} wallet 425 | * @param {string} recipient Hex-encoded recipient/smart contract address. 426 | * @param {bigint} amount Amount of PERLs to send. 427 | * @param {bigint=} gas_limit Gas limit to expend for invoking a smart contract function (optional). 428 | * @param {bigint=} gas_deposit Amount of gas to deposit into a smart contract (optional). 429 | * @param {string=} func_name Name of the function to invoke on a smart contract (optional). 430 | * @param {Uint8Array=} func_payload Binary-serialized parameters to be used to invoke a smart contract function (optional). 431 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 432 | * @returns {Promise} 433 | */ 434 | async transfer(wallet, recipient, amount, gas_limit = JSBI.BigInt(0), gas_deposit = JSBI.BigInt(0), func_name = "", func_payload = new Uint8Array(new ArrayBuffer(0)), opts = {}) { 435 | 436 | const payload = this.generatePayload(TAG_TRANSFER, recipient, amount, gas_limit, gas_deposit, func_name, func_payload); 437 | 438 | return await this.sendTransaction(wallet, TAG_TRANSFER, payload, opts); 439 | } 440 | 441 | /** 442 | * Stake some amount of PERLs which is deducted from your wallets balance. 443 | * 444 | * @param {nacl.SignKeyPair} wallet Wavelet wallet. 445 | * @param {bigint} amount Amount of PERLs to stake. 446 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 447 | * @returns {Promise<*>} 448 | */ 449 | async placeStake(wallet, amount, opts = {}) { 450 | const PLACE_STAKE = 1; 451 | const payload = this.generatePayload(TAG_STAKE, PLACE_STAKE, amount); 452 | 453 | return await this.sendTransaction(wallet, TAG_STAKE, payload, opts); 454 | } 455 | 456 | /** 457 | * Withdraw stake, which is immediately converted into PERLS into your balance. 458 | * 459 | * @param {nacl.SignKeyPair} wallet Wavelet wallet. 460 | * @param {bigint} amount Amount of PERLs to withdraw from your stake. 461 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 462 | * @returns {Promise<*>} 463 | */ 464 | async withdrawStake(wallet, amount, opts = {}) { 465 | const WITHDRAW_STAKE = 0; 466 | 467 | const payload = this.generatePayload(TAG_STAKE, WITHDRAW_STAKE, amount); 468 | 469 | return await this.sendTransaction(wallet, TAG_STAKE, payload, opts); 470 | } 471 | 472 | /** 473 | * Request a withdrawal of reward; which after some number of consensus 474 | * rounds will then convert into PERLs into your balance. 475 | * 476 | * @param {nacl.SignKeyPair} wallet Wavelet wallet. 477 | * @param {bigint} amount Amount of PERLs to request to withdraw from your rewards. 478 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 479 | * @returns {Promise<*>} 480 | */ 481 | async withdrawReward(wallet, amount, opts = {}) { 482 | const WITHDRAW_REWARD = 2; 483 | 484 | const payload = this.generatePayload(TAG_STAKE, WITHDRAW_REWARD, amount); 485 | 486 | return await this.sendTransaction(wallet, TAG_STAKE, payload, opts); 487 | } 488 | 489 | /** 490 | * Deploy a smart contract with a specified gas limit and set of parameters. 491 | * 492 | * @param {nacl.SignKeyPair} wallet Wavelet wallet. 493 | * @param {Uint8Array} code Binary of your smart contracts WebAssembly code. 494 | * @param {bigint} gas_limit Gas limit to expend for creating your smart contract, and invoking its init() function. 495 | * @param {bigint=} gas_deposit Amount of gas fees to deposit into a smart contract. 496 | * @param {Object=} params Parameters to be used for invoking your smart contracts init() function. 497 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 498 | * @returns {Promise<*>} 499 | */ 500 | async deployContract(wallet, code, gas_limit, gas_deposit = 0, params = [], opts = {}) { 501 | const payload = this.generatePayload(TAG_CONTRACT, code, gas_limit, gas_deposit, params); 502 | 503 | return await this.sendTransaction(wallet, TAG_CONTRACT, payload, opts); 504 | } 505 | 506 | 507 | /** 508 | * Calculates the transaction fee based on the payload 509 | * 510 | * @param {Uint8Array} payload Binary payload of the transaction. 511 | * @returns {number} 512 | */ 513 | calculateFee(tag, ...args) { 514 | const payload = this.generatePayload(tag, ...args); 515 | return payload.byteLength / 100 * 5; 516 | } 517 | 518 | 519 | /** 520 | * Generates payload based on the tag 521 | * 522 | * @param {number} tag Tag of the transaction. 523 | * @returns {Uint8Array} 524 | */ 525 | generatePayload(tag, ...args) { 526 | switch(tag) { 527 | case TAG_TRANSFER: 528 | return payloads.getTransfer(...args); 529 | case TAG_CONTRACT: 530 | return payloads.getContract(...args); 531 | case TAG_STAKE: 532 | return payloads.getStake(...args); 533 | default: 534 | throw Error(`No payload type found for ${tag}`); 535 | } 536 | } 537 | /** 538 | * Send a transaction on behalf of a specified wallet with a designated 539 | * tag and payload. 540 | * 541 | * @param {nacl.SignKeyPair} wallet Wavelet wallet. 542 | * @param {number} tag Tag of the transaction. 543 | * @param {Uint8Array} payload Binary payload of the transaction. 544 | * @param {Object=} opts Options to be passed on for making the specified HTTP request call (optional). 545 | * @returns {Promise<*>} 546 | */ 547 | async sendTransaction(wallet, tag, payload, opts = {}) { 548 | const payload_hex = Buffer.from(payload).toString("hex"); 549 | const sender = Buffer.from(wallet.publicKey).toString("hex"); 550 | 551 | if (typeof this.lastBlock === "undefined") { 552 | await this.initLastBlock(); 553 | } 554 | 555 | const nonce = Date.now(); 556 | const block = this.lastBlock; 557 | 558 | const signPayload = payloads.getTransaction(tag, nonce, block, payload); 559 | const signature = Buffer.from(nacl.sign.detached(signPayload, wallet.secretKey)).toString("hex"); 560 | 561 | const req = { 562 | sender, 563 | block, 564 | nonce, 565 | tag, 566 | payload: payload_hex, 567 | signature 568 | }; 569 | 570 | const data = (await axios.post(`${this.host}/tx/send`, JSON.stringify(req), {...this.opts, ...opts})).data; 571 | return { 572 | ...data, 573 | get tx_id() { 574 | console.warn("tx_id will be dreprecated. Please use id."); 575 | return data.id 576 | } 577 | }; 578 | } 579 | 580 | /** 581 | * Poll for updates to accounts. 582 | * 583 | * @param callbacks 584 | * @param {{id: string|undefined}} opts 585 | * @returns {Promise} Websocket client. 586 | */ 587 | async pollAccounts(callbacks = {}, opts = {}) { 588 | let params = {}; 589 | if (opts && opts.id && typeof opts.id === "string" && opts.id.length === 64) params.id = opts.id; 590 | 591 | return await this.pollWebsocket('/poll/accounts', params, data => { 592 | if (!Array.isArray(data)) { 593 | data = [data]; 594 | } 595 | if (callbacks && callbacks.onAccountUpdated) { 596 | data.forEach(item => { 597 | callbacks.onAccountUpdated(item) 598 | }); 599 | } 600 | }) 601 | } 602 | 603 | /** 604 | * Poll for updates to either all transactions in the ledger, or transactions made by a certain sender, or 605 | * transactions made by a certain creator, or transactions with a specific tag, or just a single transaction. 606 | * 607 | * @param callbacks 608 | * @param {{id: string|undefined, tag: number|undefined, sender: string|undefined, creator: string|undefined}} opts 609 | * @returns {Promise} Websocket client. 610 | */ 611 | async pollTransactions(callbacks = {}, opts = {}) { 612 | let params = {}; 613 | if (opts && opts.id && typeof opts.id === "string" && opts.id.length === 64) params.id = opts.id; 614 | if (opts && opts.tag && typeof opts.tag === "number") params.tag = opts.tag; 615 | if (opts && opts.sender && typeof opts.sender === "string" && opts.sender.length === 64) params.sender = opts.sender; 616 | if (opts && opts.creator && typeof opts.creator === "string" && opts.creator.length === 64) params.creator = opts.creator; 617 | return await this.pollWebsocket('/poll/tx', params, data => { 618 | if (!Array.isArray(data)) { 619 | data = [data]; 620 | } 621 | data.forEach(item => { 622 | switch (item.event) { 623 | case "failed": 624 | case "rejected": 625 | case "error": 626 | if (callbacks && callbacks.onTransactionRejected) { 627 | callbacks.onTransactionRejected(item); 628 | } 629 | break; 630 | case "applied": 631 | if (callbacks && callbacks.onTransactionApplied) { 632 | callbacks.onTransactionApplied(item); 633 | } 634 | break; 635 | } 636 | }); 637 | }) 638 | } 639 | 640 | /** 641 | * Poll for finality of consensus rounds, or the pruning of consensus rounds. 642 | * 643 | * @param callbacks 644 | * @returns {Promise} Websocket client. 645 | */ 646 | async pollConsensus(callbacks = {}) { 647 | return await this.pollWebsocket('/poll/consensus', {}, data => { 648 | switch (data.event) { 649 | case "finalized": 650 | this.lastBlock = data.new_block_height; 651 | if (callbacks && callbacks.onRoundEnded) { 652 | callbacks.onRoundEnded(data); 653 | } 654 | break; 655 | case "proposal": 656 | this.lastBlock = data.block_index; 657 | if (callbacks && callbacks.onRoundProposal) { 658 | callbacks.onRoundProposal(data); 659 | } 660 | break; 661 | } 662 | }); 663 | } 664 | 665 | /** 666 | * A generic setup function for listening for websocket events from a Wavelet node. 667 | * 668 | * @param {string} endpoint Websocket endpoint. 669 | * @param {Object=} params Query parameters to connect to the endpoint with. 670 | * @param {Object=} callback Callback function for each new event from the websocket. 671 | * @returns {Promise} Websocket client. 672 | */ 673 | pollWebsocket(endpoint, params = {}, callback = {}) { 674 | let info = url.parse(this.host); 675 | info.protocol = info.protocol === "https:" ? "wss:" : "ws:"; 676 | info.pathname = endpoint; 677 | info.query = params; 678 | 679 | return new Promise((resolve, reject) => { 680 | const client = new WebSocketClient(url.format(info)); 681 | 682 | client.onopen = () => { 683 | resolve(client); 684 | } 685 | 686 | client.onerror = () => { 687 | reject(new Error(`Failed to connect to ${url.format(info)}.`)); 688 | }; 689 | 690 | client.onmessage = msg => { 691 | if (typeof msg.data !== 'string') return; 692 | if (callback) callback(JSONbig.parse(msg.data)); 693 | }; 694 | }); 695 | } 696 | 697 | /** 698 | * Randomly generate a new Wavelet wallet. 699 | * 700 | * @returns {nacl.SignKeyPair} 701 | */ 702 | static generateNewWallet() { 703 | const c1 = 1; 704 | let generatedKeys; 705 | let checksum; 706 | 707 | const prefixLen = (buf) => { 708 | for (let i = 0; i < buf.length; i++) { 709 | const b = buf[i]; 710 | if (b !== 0) { 711 | // b.toString(2) removes leading 0s; so we just see how many were removed 712 | const leadingZeros = 8 - b.toString(2).length; 713 | 714 | return i * 8 + leadingZeros; 715 | } 716 | } 717 | 718 | return buf.length * 8 - 1; 719 | }; 720 | 721 | do { 722 | generatedKeys = nacl.sign.keyPair(); 723 | 724 | const id = blake2b(generatedKeys.publicKey, undefined, 32); 725 | checksum = blake2b(id, undefined, 32); 726 | } while (prefixLen(checksum) < c1); 727 | 728 | return generatedKeys; 729 | } 730 | 731 | /** 732 | * Load a Wavelet wallet given a hex-encoded private key. 733 | * 734 | * @param {string} private_key_hex Hex-encoded private key. 735 | * @returns {nacl.SignKeyPair} Wavelet wallet. 736 | */ 737 | static loadWalletFromPrivateKey(private_key_hex) { 738 | return nacl.sign.keyPair.fromSecretKey(Buffer.from(private_key_hex, "hex")); 739 | } 740 | 741 | /** 742 | * Parse a transactions payload content into JSON. 743 | * 744 | * @param {(TAG_TRANSFER|TAG_CONTRACT|TAG_STAKE|TAG_BATCH)} tag Tag of a transaction. 745 | * @param {string} payload Binary-serialized payload of a transaction. 746 | * @returns {{amount: bigint, recipient: string}|{}|Array|{amount: bigint}} Decoded payload of a transaction. 747 | */ 748 | static parseTransaction(tag, payload) { 749 | switch (tag) { 750 | case TAG_TRANSFER: { 751 | const buf = str2ab(atob(payload)); 752 | 753 | if (buf.byteLength < 32 + 8) { 754 | throw new Error("transfer: payload does not contain recipient id or amount"); 755 | } 756 | 757 | const view = new DataView(buf); 758 | 759 | const recipient = Buffer.from(new Uint8Array(buf, 0, 32)).toString('hex'); 760 | const amount = view.getBigUint64(32, true); 761 | 762 | let tx = {recipient, amount}; 763 | 764 | if (buf.byteLength > 32 + 8) { 765 | tx.gasLimit = view.getBigUint64(32 + 8, true); 766 | tx.gasDeposit = view.getBigUint64(32 + 8 + 8, true); 767 | 768 | const funcNameLen = view.getUint32(32 + 8 + 8 + 8, true); 769 | tx.funcName = Buffer.from(new Uint8Array(buf, 32 + 8 + 8 + 8 + 4, funcNameLen)).toString("utf8"); 770 | 771 | const funcPayloadLen = view.getUint32(32 + 8 + 8 + 8 + 4 + funcNameLen, true); 772 | tx.payload = Buffer.from(new Uint8Array(buf, 32 + 8 + 8 + 8 + 4 + funcNameLen + 4, funcPayloadLen)); 773 | } 774 | 775 | return tx; 776 | } 777 | case TAG_CONTRACT: { 778 | const buf = str2ab(atob(payload)); 779 | 780 | if (buf.byteLength < 12) { 781 | throw new Error("contract: payload is malformed"); 782 | } 783 | 784 | const view = new DataView(buf); 785 | 786 | let tx = {}; 787 | 788 | tx.gasLimit = view.getBigUint64(0, true); 789 | tx.gasDeposit = view.getBigUint64(8, true); 790 | 791 | const payloadLen = view.getUint32(8 + 8, true); 792 | 793 | tx.payload = Buffer.from(new Uint8Array(buf, 8 + 8 + 4, payloadLen)); 794 | tx.code = Buffer.from(new Uint8Array(buf, 8 + 8 + 4 + payloadLen)); 795 | 796 | return tx; 797 | } 798 | case TAG_STAKE: { 799 | const buf = str2ab(atob(payload)); 800 | 801 | if (buf.byteLength !== 9) { 802 | throw new Error("stake: payload must be exactly 9 bytes"); 803 | } 804 | 805 | const view = new DataView(buf); 806 | const opcode = view.getUint8(0); 807 | 808 | if (opcode < 0 || opcode > 2) { 809 | throw new Error("stake: opcode must be between 0 to 2") 810 | } 811 | 812 | const amount = view.getBigUint64(1, true); 813 | 814 | let tx = {amount}; 815 | 816 | switch (opcode) { 817 | case 0: 818 | tx.op = "withdraw_stake"; 819 | break; 820 | case 1: 821 | tx.op = "place_stake"; 822 | break; 823 | case 2: 824 | tx.op = "withdraw_reward"; 825 | break; 826 | } 827 | 828 | return tx; 829 | } 830 | case TAG_BATCH: { 831 | const buf = str2ab(atob(payload)); 832 | const view = new DataView(buf); 833 | 834 | const len = view.getUint8(0); 835 | 836 | let transactions = []; 837 | 838 | for (let i = 0, offset = 1; i < len; i++) { 839 | const tag = view.getUint8(offset); 840 | offset += 1; 841 | 842 | const payloadLen = view.getUint32(offset, true); 843 | offset += 4; 844 | 845 | const payload = Buffer.from(new Uint8Array(buf, offset, payloadLen)); 846 | offset += payloadLen; 847 | 848 | transactions.push(this.parseTransaction(tag, payload)); 849 | } 850 | 851 | return transactions; 852 | } 853 | default: 854 | throw new Error(`unknown tag type: ${tag}`); 855 | } 856 | } 857 | } 858 | 859 | export { Wavelet, Contract, TAG_TRANSFER, TAG_CONTRACT, TAG_STAKE, TAG_BATCH, JSBI, Buffer }; 860 | -------------------------------------------------------------------------------- /src/payload-builder.js: -------------------------------------------------------------------------------- 1 | export default class PayloadBuilder { 2 | /** 3 | * A payload builder made for easier handling of binary serialization of 4 | * data for Wavelet to ingest. 5 | */ 6 | constructor() { 7 | this.buf = new ArrayBuffer(0); 8 | this.view = new DataView(this.buf); 9 | this.offset = 0; 10 | } 11 | 12 | /** 13 | * Resizes the underlying buffer should it not be large enough to handle 14 | * some chunk of data to be appended to buffer. 15 | * 16 | * @param {number} size Size of data to be appended to the buffer. 17 | */ 18 | resizeIfNeeded(size) { 19 | if (this.offset + size > this.buf.byteLength) { 20 | this.buf = ArrayBuffer.transfer(this.buf, this.offset + size); 21 | this.view = new DataView(this.buf); 22 | } 23 | } 24 | 25 | /** 26 | * Write a single byte to the payload buffer. 27 | * 28 | * @param {number} n A single byte. 29 | */ 30 | writeByte(n) { 31 | this.resizeIfNeeded(1); 32 | this.view.setUint8(this.offset, n); 33 | this.offset += 1; 34 | } 35 | 36 | /** 37 | * Write an signed little-endian 16-bit integer to the payload buffer. 38 | * 39 | * @param {number} n 40 | */ 41 | writeInt16(n) { 42 | this.resizeIfNeeded(2); 43 | this.view.setInt16(this.offset, n, true); 44 | this.offset += 2; 45 | } 46 | 47 | /** 48 | * Write an signed little-endian 32-bit integer to the payload buffer. 49 | * 50 | * @param {number} n 51 | */ 52 | writeInt32(n) { 53 | this.resizeIfNeeded(4); 54 | this.view.setInt32(this.offset, n, true); 55 | this.offset += 4; 56 | } 57 | 58 | /** 59 | * Write a signed little-endian 64-bit integer to the payload buffer. 60 | * 61 | * @param {bigint} n 62 | */ 63 | writeInt64(n) { 64 | this.resizeIfNeeded(8); 65 | this.view.setBigInt64(this.offset, n, true); 66 | this.offset += 8; 67 | } 68 | 69 | /** 70 | * Write an unsigned little-endian 16-bit integer to the payload buffer. 71 | * 72 | * @param {number} n 73 | */ 74 | writeUint16(n) { 75 | this.resizeIfNeeded(2); 76 | this.view.setUint16(this.offset, n, true); 77 | this.offset += 2; 78 | } 79 | 80 | /** 81 | * Write an unsigned little-endian 32-bit integer to the payload buffer. 82 | * 83 | * @param {number} n 84 | */ 85 | writeUint32(n) { 86 | this.resizeIfNeeded(4); 87 | this.view.setUint32(this.offset, n, true); 88 | this.offset += 4; 89 | } 90 | 91 | /** 92 | * Write an unsigned little-endian 64-bit integer to the payload buffer. 93 | * 94 | * @param {bigint} n 95 | */ 96 | writeUint64(n) { 97 | this.resizeIfNeeded(8); 98 | this.view.setBigUint64(this.offset, n, true); 99 | this.offset += 8; 100 | } 101 | 102 | /** 103 | * Write a series of bytes to the payload buffer. 104 | * 105 | * @param {ArrayBufferLike} buf 106 | */ 107 | writeBytes(buf) { 108 | this.resizeIfNeeded(buf.byteLength); 109 | new Uint8Array(this.buf, this.offset, buf.byteLength).set(buf); 110 | this.offset += buf.byteLength; 111 | } 112 | 113 | /** 114 | * Returns the raw bytes of the payload buffer. 115 | * 116 | * @returns {Uint8Array} 117 | */ 118 | getBytes() { 119 | return new Uint8Array(this.buf.slice(0, this.offset)); 120 | } 121 | } -------------------------------------------------------------------------------- /src/payloads.js: -------------------------------------------------------------------------------- 1 | import PayloadBuilder from "./payload-builder"; 2 | import { toBufferBE } from "bigint-buffer"; 3 | import JSBI from "jsbi"; 4 | 5 | if (typeof window === 'undefined') { 6 | var window = window || {}; 7 | var global = global || window; 8 | } 9 | 10 | export const normalizeNumber = value => { 11 | if (typeof value !== "bigint" || value.constructor !== JSBI) { 12 | return JSBI.BigInt(value + ""); 13 | } 14 | value; 15 | }; 16 | 17 | export const getTransfer = ( 18 | recipient, 19 | amount, 20 | gas_limit = 0, 21 | gas_deposit = 0, 22 | func_name = "", 23 | func_payload = new Uint8Array(new ArrayBuffer(0)) 24 | ) => { 25 | const builder = new PayloadBuilder(); 26 | 27 | builder.writeBytes(Buffer.from(recipient, "hex")); 28 | 29 | amount = normalizeNumber(amount); 30 | gas_limit = normalizeNumber(gas_limit); 31 | gas_deposit = normalizeNumber(gas_deposit); 32 | 33 | builder.writeUint64(amount); 34 | 35 | if ( 36 | JSBI.GT(gas_limit, JSBI.BigInt(0)) || 37 | func_name.length > 0 || 38 | func_payload.length > 0 39 | ) { 40 | if (func_name.length === 0 && JSBI.GT(amount, BigInt(0))) { 41 | // Default to 'on_money_received' if no func name is specified. 42 | func_name = "on_money_received"; 43 | } 44 | 45 | const func_name_buf = Buffer.from(func_name, "utf8"); 46 | const func_payload_buf = new Uint8Array(func_payload); 47 | 48 | builder.writeUint64(gas_limit); 49 | builder.writeUint64(gas_deposit); 50 | 51 | builder.writeUint32(func_name_buf.byteLength); 52 | builder.writeBytes(func_name_buf); 53 | 54 | builder.writeUint32(func_payload_buf.byteLength); 55 | builder.writeBytes(func_payload_buf); 56 | } 57 | 58 | const payload = builder.getBytes(); 59 | return payload; 60 | }; 61 | 62 | export const getContract = (code, gas_limit = 0, gas_deposit = 0, params = []) => { 63 | gas_limit = normalizeNumber(gas_limit); 64 | gas_deposit = normalizeNumber(gas_deposit); 65 | 66 | code = new Uint8Array(code); 67 | params = new Uint8Array(params); 68 | 69 | const builder = new PayloadBuilder(); 70 | 71 | builder.writeUint64(gas_limit); 72 | builder.writeUint64(gas_deposit); 73 | builder.writeUint32(params.byteLength); 74 | builder.writeBytes(params); 75 | builder.writeBytes(code); 76 | 77 | const payload = builder.getBytes(); 78 | return payload; 79 | }; 80 | 81 | export const getStake = (stakeByte, amount = 0) => { 82 | amount = normalizeNumber(amount); 83 | 84 | const builder = new PayloadBuilder(); 85 | 86 | builder.writeByte(stakeByte); 87 | builder.writeUint64(amount); 88 | 89 | const payload = builder.getBytes(); 90 | return payload; 91 | }; 92 | 93 | /** 94 | * Parses smart contract function parameters as a variadic list of arguments, and translates 95 | * them into an array of bytes suitable for passing on to a single smart contract invocation call. 96 | * 97 | * @param {...{type: ('int16'|'int32'|'int64'|'uint16'|'uint32'|'uint64'|'byte'|'raw'|'bytes'|'string'), value: number|string|ArrayBuffer|Uint8Array}} params Variadic list of arguments. 98 | * @returns {Uint8Array} Parameters serialized into bytes. 99 | */ 100 | export const parseFunctionParams = (...params) => { 101 | const builder = new PayloadBuilder(); 102 | 103 | params.forEach(param => { 104 | switch (param.type) { 105 | case "int16": 106 | builder.writeInt16(param.value); 107 | break; 108 | case "int32": 109 | builder.writeInt32(param.value); 110 | break; 111 | case "int64": 112 | builder.writeInt64(param.value); 113 | case "uint16": 114 | builder.writeUint16(param.value); 115 | break; 116 | case "uint32": 117 | builder.writeUint32(param.value); 118 | break; 119 | case "uint64": 120 | builder.writeUint64(param.value); 121 | break; 122 | case "byte": 123 | builder.writeByte(param.value); 124 | break; 125 | case "raw": 126 | if (typeof param.value === "string") { 127 | // Assume that it is hex-encoded. 128 | param.value = new Uint8Array( 129 | param.value 130 | .match(/[\da-f]{2}/gi) 131 | .map(h => parseInt(h, 16)) 132 | ); 133 | } 134 | 135 | builder.writeBytes(param.value); 136 | break; 137 | case "bytes": 138 | if (typeof param.value === "string") { 139 | // Assume that it is hex-encoded. 140 | param.value = new Uint8Array( 141 | param.value 142 | .match(/[\da-f]{2}/gi) 143 | .map(h => parseInt(h, 16)) 144 | ); 145 | } 146 | 147 | builder.writeUint32(param.value.byteLength); 148 | builder.writeBytes(param.value); 149 | break; 150 | case "string": 151 | builder.writeBytes(Buffer.from(param.value, "utf8")); 152 | builder.writeByte(0); 153 | break; 154 | } 155 | }); 156 | 157 | const payload = builder.getBytes(); 158 | return payload; 159 | }; 160 | 161 | export const getTransaction = (tag, nonce, block, innerPayload) => { 162 | 163 | // toBufferBE breaks in node as it expects BigInt values 164 | if (typeof BigInt !== "undefined") { 165 | nonce = BigInt(nonce); 166 | block = BigInt(block); 167 | } 168 | const binNonce = toBufferBE(nonce, 8); 169 | const binBlock = toBufferBE(block, 8); 170 | const builder = new PayloadBuilder(); 171 | 172 | builder.writeBytes(binNonce); 173 | builder.writeBytes(binBlock); 174 | builder.writeByte(tag); 175 | builder.writeBytes(innerPayload); 176 | 177 | const payload = builder.getBytes(); 178 | return payload; 179 | }; 180 | 181 | /** 182 | * Based on updates to simulation settings for this smart contract, re-build the 183 | * smart contracts payload. 184 | */ 185 | export const rebuildContractPayload = contract_payload => { 186 | const builder = new PayloadBuilder(); 187 | builder.writeUint64(contract_payload.round_idx); 188 | builder.writeBytes(Buffer.from(contract_payload.round_id, "hex")); 189 | builder.writeBytes(Buffer.from(contract_payload.transaction_id, "hex")); 190 | builder.writeBytes(Buffer.from(contract_payload.sender_id, "hex")); 191 | builder.writeUint64(contract_payload.amount); 192 | builder.writeBytes(contract_payload.params); 193 | 194 | const payload = builder.getBytes(); 195 | return payload; 196 | }; 197 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | const { Wavelet, Contract, TAG_TRANSFER, JSBI, Buffer } = window["wavelet-client"]; 2 | 3 | const client = new Wavelet("https://devnet.perlin.net"); 4 | 5 | (async () => { 6 | console.log(Wavelet.generateNewWallet()); 7 | const wallet = Wavelet.loadWalletFromPrivateKey( 8 | "ba3daa36b1612a30fb0f7783f98eb508e8f045ffb042124f86281fb41aee8705e919a3626df31b6114ec79567726e9a31c600a5d192e871de1b862412ae8e4c0" 9 | ); 10 | 11 | const accountResponse = await client.getAccount( 12 | "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" 13 | ); 14 | console.log("account", accountResponse); 15 | 16 | try { 17 | const txResponse = await client.transfer( 18 | wallet, 19 | "f03bb6f98c4dfd31f3d448c7ec79fa3eaa92250112ada43471812f4b1ace6467", 20 | 0 21 | ); 22 | console.log("txResponse", txResponse); 23 | await client.pollTransactions( 24 | { 25 | onTransactionApplied: data => { 26 | console.log("tx applied", data); 27 | } 28 | }, 29 | { 30 | id: txResponse.id 31 | } 32 | ); 33 | 34 | const transfer = await client.getTransaction( 35 | txResponse.id 36 | ); 37 | console.log(Wavelet.parseTransaction(transfer.tag, transfer.payload)); 38 | } catch (err) { 39 | alert(err.message || err); 40 | } 41 | 42 | const account = await client.getAccount( 43 | Buffer.from(wallet.publicKey).toString("hex") 44 | ); 45 | 46 | console.log("account", account); 47 | const contract = new Contract( 48 | client, 49 | "4b6b43eba9eb8ed7402e0b7103a3eab9dfba48be05e359557d8c71f6a8513563" 50 | ); 51 | await contract.init(); 52 | 53 | console.log( 54 | contract.test(wallet, "balance", BigInt(0), { 55 | type: "raw", 56 | value: "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" 57 | }) 58 | ); 59 | 60 | console.log( 61 | await contract.call( 62 | wallet, 63 | "balance", 64 | BigInt(0), 65 | JSBI.subtract(JSBI.BigInt(account.balance), JSBI.BigInt(1000000)), 66 | 1000n, 67 | { 68 | type: "raw", 69 | value: 70 | "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" 71 | } 72 | ) 73 | ); 74 | 75 | const consensusPoll = await client.pollConsensus({ 76 | onRoundEnded: console.log 77 | }); 78 | const transactionsPoll = await client.pollTransactions( 79 | { onTransactionApplied: console.log }, 80 | { 81 | tag: TAG_TRANSFER, 82 | creator: 83 | "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" 84 | } 85 | ); 86 | const accountsPoll = await client.pollAccounts( 87 | { onAccountUpdated: console.log }, 88 | { id: "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" } 89 | ); 90 | 91 | for (let i = 0; i < 100; i++) { 92 | await client.transfer( 93 | wallet, 94 | "e49e8be205a00edb45de8183a4374e362efc9a4da56dd7ba17e2dd780501e49f", 95 | BigInt(1000000) 96 | ); 97 | } 98 | })(); 99 | -------------------------------------------------------------------------------- /test/node.js: -------------------------------------------------------------------------------- 1 | const { Wavelet, Contract, TAG_TRANSFER, JSBI, Buffer } = require(".."); //window["wavelet-client"]; 2 | const util = require("util"); 3 | const readFile = util.promisify(require("fs").readFile); 4 | const path = require("path"); 5 | const BigInt = JSBI.BigInt; 6 | 7 | const client = new Wavelet("https://devnet.perlin.net"); 8 | 9 | function toArrayBuffer(buffer) { 10 | var ab = new ArrayBuffer(buffer.length); 11 | var view = new Uint8Array(ab); 12 | for (var i = 0; i < buffer.length; ++i) { 13 | view[i] = buffer[i]; 14 | } 15 | return ab; 16 | } 17 | 18 | (async () => { 19 | console.log(Wavelet.generateNewWallet()); 20 | const wallet = Wavelet.loadWalletFromPrivateKey( 21 | "ba3daa36b1612a30fb0f7783f98eb508e8f045ffb042124f86281fb41aee8705e919a3626df31b6114ec79567726e9a31c600a5d192e871de1b862412ae8e4c0" 22 | ); 23 | 24 | const contractCode = await readFile(path.resolve(__dirname, "./token_bg.wasm")); 25 | console.log("Contract", contractCode); 26 | const contractId = await client.deployContract(wallet, toArrayBuffer(contractCode), JSBI.BigInt(100000)); 27 | console.log("res", contractId); 28 | 29 | console.log(await client.getNodeInfo()); 30 | 31 | const accountResponse = await client.getAccount( 32 | "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" 33 | ); 34 | console.log("account", accountResponse); 35 | 36 | try { 37 | const txResponse = await client.transfer( 38 | wallet, 39 | "f03bb6f98c4dfd31f3d448c7ec79fa3eaa92250112ada43471812f4b1ace6467", 40 | 0 41 | ); 42 | console.log("txResponse", txResponse); 43 | await client.pollTransactions( 44 | { 45 | onTransactionApplied: data => { 46 | console.log("tx applied", data); 47 | } 48 | }, 49 | { 50 | id: txResponse.id 51 | } 52 | ); 53 | 54 | const transfer = await client.getTransaction( 55 | txResponse.id 56 | ); 57 | console.log(Wavelet.parseTransaction(transfer.tag, transfer.payload)); 58 | } catch (err) { 59 | alert(err.message || err); 60 | } 61 | 62 | 63 | const account = await client.getAccount( 64 | Buffer.from(wallet.publicKey).toString("hex") 65 | ); 66 | 67 | console.log("account", account); 68 | const contract = new Contract( 69 | client, 70 | "4b6b43eba9eb8ed7402e0b7103a3eab9dfba48be05e359557d8c71f6a8513563" 71 | ); 72 | await contract.init(); 73 | 74 | console.log( 75 | contract.test(wallet, "balance", BigInt(0), { 76 | type: "raw", 77 | value: "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" 78 | }) 79 | ); 80 | 81 | console.log( 82 | await contract.call( 83 | wallet, 84 | "balance", 85 | BigInt(0), 86 | JSBI.subtract(JSBI.BigInt(account.balance), JSBI.BigInt(1000000)), 87 | 1000n, 88 | { 89 | type: "raw", 90 | value: 91 | "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" 92 | } 93 | ) 94 | ); 95 | 96 | const consensusPoll = await client.pollConsensus({ 97 | onRoundEnded: console.log 98 | }); 99 | const transactionsPoll = await client.pollTransactions( 100 | { onTransactionApplied: console.log }, 101 | { 102 | tag: TAG_TRANSFER, 103 | creator: 104 | "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" 105 | } 106 | ); 107 | const accountsPoll = await client.pollAccounts( 108 | { onAccountUpdated: console.log }, 109 | { id: "400056ee68a7cc2695222df05ea76875bc27ec6e61e8e62317c336157019c405" } 110 | ); 111 | 112 | for (let i = 0; i < 100; i++) { 113 | await client.transfer( 114 | wallet, 115 | "e49e8be205a00edb45de8183a4374e362efc9a4da56dd7ba17e2dd780501e49f", 116 | BigInt(1000000) 117 | ); 118 | } 119 | })(); 120 | -------------------------------------------------------------------------------- /test/token_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perlin-network/wavelet-client-js/df6ed6a3a77ee579ecc09b1083bacc4668a4fd14/test/token_bg.wasm -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./src/main.js", 5 | output: { 6 | path: path.resolve(__dirname, "dist"), 7 | filename: "wavelet-client.umd.js", 8 | library: "wavelet-client", 9 | libraryTarget: "umd", 10 | globalObject: `(typeof self !== 'undefined' ? self : this)` 11 | }, 12 | mode: "development", 13 | resolve: { 14 | alias: { 15 | // websocket: path.resolve(__dirname, 'node_modules/websocket/lib/websocket.js'), 16 | atob: path.resolve(__dirname, "node_modules/atob/node-atob.js") 17 | } 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js)$/, 23 | exclude: /node_modules/, 24 | use: ["babel-loader"] 25 | } 26 | ] 27 | } 28 | }; 29 | --------------------------------------------------------------------------------