├── .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 | [](https://www.npmjs.com/package/wavelet-client)
4 | [](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