├── .gitignore ├── .npmignore ├── README.md ├── deploy-test-contracts.sh ├── example ├── AVar.sol ├── aVar.json └── setA.js ├── package.json ├── src ├── Contract.ts ├── Contract_test.ts ├── ContractsRepo.ts ├── ContractsRepo_test.ts ├── EventListener.ts ├── EventListener_test.ts ├── MethodMap.ts ├── MethodMap_test.ts ├── Qtum.ts ├── QtumRPC.ts ├── QtumRPCRaw.ts ├── QtumRPC_test.ts ├── Qtum_test.ts ├── TxReceiptPromise.ts ├── abi.ts ├── ethjs-abi.ts ├── index.ts ├── sleep.ts └── test │ └── index.ts ├── test └── contracts │ ├── ArrayArguments.sol │ ├── LogOfDependantContract.sol │ ├── LogOfDependantContractChild.sol │ ├── Logs.sol │ ├── MethodOverloading.sol │ └── Methods.sol ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | dist/ 3 | .vscode/ 4 | node_modules/ 5 | .DS_Store 6 | .qtum 7 | .cache 8 | solar.development.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | TODO.md 3 | .vscode 4 | yarn* 5 | .env 6 | src 7 | .qtum 8 | .cache 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The QTUM JavaScript library for Smart Contract development. 2 | 3 | See [documentation](https://qtumproject.github.io/qtumjs-doc/). 4 | 5 | See [中文 API 文档](https://qtumproject.github.io/qtumjs-doc-cn/). 6 | 7 | See [companion tutorial](https://github.com/qtumproject/qtumbook/blob/master/en/part2/erc20-js.md). 8 | 9 | # Install 10 | 11 | ``` 12 | npm install qtumjs 13 | ``` 14 | 15 | This is a sample code snippet that transfer ERC20 tokens: 16 | 17 | ```js 18 | import { QtumRPC } from "qtumjs" 19 | 20 | const repoData = require("./solar.json") 21 | const qtum = new Qtum("http://qtum:test@localhost:3889", repoData) 22 | 23 | const myToken = qtum.contract( 24 | "zeppelin-solidity/contracts/token/CappedToken.sol", 25 | ) 26 | 27 | async function transfer(fromAddr, toAddr, amount) { 28 | const tx = await myToken.send("transfer", [toAddr, amount], { 29 | senderAddress: fromAddr, 30 | }) 31 | 32 | console.log("transfer tx:", tx.txid) 33 | console.log(tx) 34 | 35 | await tx.confirm(3) 36 | console.log("transfer confirmed") 37 | } 38 | ``` 39 | 40 | The [full source code](https://github.com/qtumproject/qtumbook-mytoken-qtumjs-cli). 41 | 42 | > This example uses async/await (supported natively by Node 8+). 43 | 44 | # Running Tests 45 | 46 | ``` 47 | docker run -it --rm \ 48 | --name qtumjs \ 49 | -v `pwd`:/dapp \ 50 | -p 3889:3889 \ 51 | hayeah/qtumportal 52 | ``` 53 | 54 | Configure QTUM_RPC for deployment tool: 55 | 56 | Enter into container: 57 | 58 | ``` 59 | docker exec -it qtumjs sh 60 | ``` 61 | 62 | Generate initial blocks: 63 | 64 | ``` 65 | qcli importprivkey cMbgxCJrTYUqgcmiC1berh5DFrtY1KeU4PXZ6NZxgenniF1mXCRk 66 | qcli generatetoaddress 600 qUbxboqjBRp96j3La8D1RYkyqx5uQbJPoW 67 | 68 | qcli getbalance 69 | 70 | 2000000.00000000 71 | ``` 72 | 73 | Deploy test contracts: 74 | 75 | ``` 76 | export QTUM_RPC=http://qtum:test@localhost:3889 77 | export QTUM_SENDER=qUbxboqjBRp96j3La8D1RYkyqx5uQbJPoW 78 | 79 | sh deploy-test-contracts.sh 80 | ``` 81 | 82 | Build and run tests: 83 | 84 | ``` 85 | npm build 86 | npm run test 87 | ``` 88 | -------------------------------------------------------------------------------- /deploy-test-contracts.sh: -------------------------------------------------------------------------------- 1 | solar deploy test/contracts/MethodOverloading.sol --force 2 | solar deploy test/contracts/Methods.sol --force 3 | solar deploy test/contracts/Logs.sol --force 4 | solar deploy test/contracts/LogOfDependantContract.sol --force 5 | solar deploy test/contracts/ArrayArguments.sol --force 6 | solar deploy test/contracts/SenderFrom.sol --force 7 | -------------------------------------------------------------------------------- /example/AVar.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | contract AVar { 4 | uint256 a; 5 | 6 | function setA(uint256 _a) { 7 | a = _a; 8 | } 9 | 10 | function getA() returns(uint256) { 11 | return a; 12 | } 13 | } -------------------------------------------------------------------------------- /example/aVar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AVar", 3 | "deployName": "aVar", 4 | "address": "05ce94dc5d7c07cb094aed438bf13e5615b82011", 5 | "txid": "4ddd9eb2c66b6471cc804e93ea5f2a08cd5f62fe747fa075ffbbd3254e82faa3", 6 | "abi": [ 7 | { 8 | "name": "getA", 9 | "type": "function", 10 | "payable": false, 11 | "inputs": [], 12 | "outputs": [ 13 | { 14 | "name": "", 15 | "type": "uint256" 16 | } 17 | ], 18 | "constant": false 19 | }, 20 | { 21 | "name": "setA", 22 | "type": "function", 23 | "payable": false, 24 | "inputs": [ 25 | { 26 | "name": "_a", 27 | "type": "uint256" 28 | } 29 | ], 30 | "outputs": [], 31 | "constant": false 32 | } 33 | ], 34 | "bin": "6060604052341561000f57600080fd5b5b60b98061001e6000396000f300606060405263ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663d46300fd81146046578063ee919d50146068575b600080fd5b3415605057600080fd5b6056607d565b60405190815260200160405180910390f35b3415607257600080fd5b607b6004356084565b005b6000545b90565b60008190555b505600a165627a7a7230582040e87d0c62a01d8dd32ea10840e9f0b3628cb880d9a323b5c07709217591f9d90029", 35 | "binhash": "42712271c9f5e5dcd27eaeb999bf4388eb80c55cd652980a7b22aa34f774d76b", 36 | "createdAt": "2017-09-20T11:15:16.25028283+08:00", 37 | "confirmed": true 38 | } -------------------------------------------------------------------------------- /example/setA.js: -------------------------------------------------------------------------------- 1 | const { Contract, QtumRPC } = require("qtumjs") 2 | 3 | const rpc = new QtumRPC("http://howard:yeh@localhost:13889") 4 | 5 | async function main() { 6 | // Load the ABI and address of a deployed contract 7 | const contractInfo = require("./aVar.json") 8 | 9 | // Instantiate an RPC client that interacts with the contract using ABI encoding. 10 | const foo = new Contract(rpc, contractInfo) 11 | 12 | // Create a transaction that calls setA with a random integer 13 | const i = Math.floor(Math.random() * 100) 14 | console.log("setA", i) 15 | const receipt = await foo.send("setA", [i]) 16 | 17 | // Wait for transaction to confirm (wait for 1 block) 18 | console.log("txid", receipt.txid) 19 | console.log("waiting for transaction confirmation") 20 | await receipt.done(1) 21 | 22 | // Make an RPC call of a constant function 23 | const callResult = await foo.call("getA") 24 | 25 | return { 26 | // First return value 27 | r0: callResult[0], 28 | // Other metadata about the call (e.g. gas used) 29 | callResult, 30 | } 31 | } 32 | 33 | main().then((result) => { 34 | console.log("ok") 35 | console.log(result) 36 | }).catch((err) => { 37 | console.log("err", err) 38 | }) 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qtumjs", 3 | "version": "1.9.3", 4 | "main": "./lib/index.js", 5 | "types": "./lib/index.d.ts", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "mocha lib/*_test.js", 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "lint": "tslint --project tsconfig.json", 12 | "build:umd": "webpack --config webpack.config.js" 13 | }, 14 | "npmName": "qtumjs", 15 | "npmFileMap": [ 16 | { 17 | "basePath": "/dist/", 18 | "files": [ 19 | "*.js" 20 | ] 21 | } 22 | ], 23 | "dependencies": { 24 | "@types/debug": "^4.1.5", 25 | "axios": "^0.19.2", 26 | "btoa": "^1.1.2", 27 | "debug": "^4.1.1", 28 | "eventemitter3": "^2.0.3", 29 | "qtumjs-ethjs-abi": "^1.0.3", 30 | "url-parse": "^1.4.7" 31 | }, 32 | "devDependencies": { 33 | "@types/chai": "^4.2.12", 34 | "@types/chai-as-promised": "^7.1.3", 35 | "@types/mocha": "^8.0.3", 36 | "@types/node": "^14.6.0", 37 | "chai": "^4.2.0", 38 | "chai-as-promised": "^7.1.1", 39 | "mocha": "^8.1.1", 40 | "ts-loader": "^8.0.2", 41 | "tslint": "^6.1.3", 42 | "typescript": "^3.9.7", 43 | "webpack": "^4.44.1", 44 | "webpack-cli": "^3.3.12" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Contract.ts: -------------------------------------------------------------------------------- 1 | import { IABIMethod, IETHABI, LogDecoder } from "./ethjs-abi" 2 | import { EventEmitter } from "eventemitter3" 3 | 4 | import debug from "debug" 5 | 6 | const log = debug("qtumjs:contract") 7 | 8 | const { logDecoder } = require("qtumjs-ethjs-abi") as IETHABI 9 | 10 | import { decodeOutputs, encodeInputs, ContractLogDecoder } from "./abi" 11 | 12 | import { 13 | IDecodedSolidityEvent, 14 | ITransactionLog, 15 | IRPCSearchLogsRequest, 16 | } from "./index" 17 | import { 18 | IExecutionResult, 19 | IRPCCallContractResult, 20 | IRPCGetTransactionReceiptBase, 21 | IRPCGetTransactionReceiptResult, 22 | IRPCGetTransactionResult, 23 | IRPCSendToContractResult, 24 | IRPCWaitForLogsResult, 25 | QtumRPC, 26 | IRPCWaitForLogsRequest, 27 | ILogEntry, 28 | } from "./QtumRPC" 29 | 30 | import { 31 | TxReceiptConfirmationHandler, 32 | TxReceiptPromise, 33 | } from "./TxReceiptPromise" 34 | 35 | import { MethodMap } from "./MethodMap" 36 | 37 | export interface IContractSendTx { 38 | method: string 39 | txid: string 40 | } 41 | 42 | /** 43 | * The callback function invoked for each additional confirmation 44 | */ 45 | export type IContractSendConfirmationHandler = ( 46 | tx: IRPCGetTransactionResult, 47 | receipt: IContractSendReceipt, 48 | ) => any 49 | 50 | /** 51 | * @param n Number of confirmations to wait for 52 | * @param handler The callback function invoked for each additional confirmation 53 | */ 54 | export type IContractSendConfirmFunction = ( 55 | n?: number, 56 | handler?: IContractSendConfirmationHandler, 57 | ) => Promise 58 | 59 | /** 60 | * Result of contract send. 61 | */ 62 | export interface IContractSendResult extends IRPCGetTransactionResult { 63 | /** 64 | * Name of contract method invoked. 65 | */ 66 | method: string 67 | 68 | /** 69 | * Wait for transaction confirmations. 70 | */ 71 | confirm: IContractSendConfirmFunction 72 | } 73 | 74 | /** 75 | * The minimal deployment information necessary to interact with a 76 | * deployed contract. 77 | */ 78 | export interface IContractInfo { 79 | /** 80 | * Contract's ABI definitions, produced by solc. 81 | */ 82 | abi: IABIMethod[] 83 | 84 | /** 85 | * Contract's address 86 | */ 87 | address: string 88 | 89 | /** 90 | * The owner address of the contract 91 | */ 92 | sender?: string 93 | } 94 | 95 | /** 96 | * Deployment information stored by solar 97 | */ 98 | export interface IDeployedContractInfo extends IContractInfo { 99 | name: string 100 | deployName: string 101 | txid: string 102 | bin: string 103 | binhash: string 104 | createdAt: string // date string 105 | confirmed: boolean 106 | } 107 | 108 | /** 109 | * The result of calling a contract method, with decoded outputs. 110 | */ 111 | export interface IContractCallResult extends IRPCCallContractResult { 112 | /** 113 | * ABI-decoded outputs 114 | */ 115 | outputs: any[] 116 | 117 | /** 118 | * ABI-decoded logs 119 | */ 120 | logs: (IDecodedSolidityEvent | null)[] 121 | } 122 | 123 | /** 124 | * Options for `send` to a contract method. 125 | */ 126 | export interface IContractSendRequestOptions { 127 | /** 128 | * The amount in QTUM to send. eg 0.1, default: 0 129 | */ 130 | amount?: number | string 131 | 132 | /** 133 | * gasLimit, default: 200000, max: 40000000 134 | */ 135 | gasLimit?: number 136 | 137 | /** 138 | * Qtum price per gas unit, default: 0.00000001, min:0.00000001 139 | */ 140 | gasPrice?: number | string 141 | 142 | /** 143 | * The quantum address that will be used as sender. 144 | */ 145 | senderAddress?: string 146 | } 147 | 148 | /** 149 | * Options for `call` to a contract method. 150 | */ 151 | export interface IContractCallRequestOptions { 152 | /** 153 | * The quantum address that will be used as sender. 154 | */ 155 | senderAddress?: string 156 | gasLimit?: number 157 | amount?: number 158 | } 159 | 160 | /** 161 | * The transaction receipt for a `send` to a contract method, with the event 162 | * logs decoded. 163 | */ 164 | export interface IContractSendReceipt extends IRPCGetTransactionReceiptBase { 165 | /** 166 | * logs decoded using ABI 167 | */ 168 | logs: IDecodedSolidityEvent[] 169 | 170 | /** 171 | * undecoded logs 172 | */ 173 | rawlogs: ITransactionLog[] 174 | } 175 | 176 | export interface IContractLog extends ILogEntry { 177 | event: T 178 | } 179 | 180 | /** 181 | * A decoded contract event log. 182 | */ 183 | export interface IContractEventLog extends ILogEntry { 184 | /** 185 | * Solidity event, ABI decoded. Null if no ABI definition is found. 186 | */ 187 | event?: IDecodedSolidityEvent | null 188 | } 189 | 190 | /** 191 | * Query result of a contract's event logs. 192 | */ 193 | export interface IContractEventLogs { 194 | /** 195 | * Event logs, ABI decoded. 196 | */ 197 | entries: IContractEventLog[] 198 | 199 | /** 200 | * Number of event logs returned. 201 | */ 202 | count: number 203 | 204 | /** 205 | * The block number to start query for new event logs. 206 | */ 207 | nextblock: number 208 | } 209 | 210 | export interface IContractInitOptions { 211 | /** 212 | * event logs decoder. It may know how to decode logs not whose types are not 213 | * defined in this particular contract's `info`. Typically ContractsRepo would 214 | * pass in a logDecoder that knows about all the event definitions. 215 | */ 216 | logDecoder?: ContractLogDecoder 217 | 218 | /** 219 | * If a contract's use case requires numbers more than 53 bits, use bn.js to 220 | * represent numbers instead of native JavaScript numbers. (default = false) 221 | */ 222 | useBigNumber?: boolean 223 | } 224 | 225 | /** 226 | * Contract represents a Smart Contract deployed on the blockchain. 227 | */ 228 | export class Contract { 229 | /** 230 | * The contract's address as hex160 231 | */ 232 | public address: string 233 | 234 | private methodMap: MethodMap 235 | private _logDecoder: ContractLogDecoder 236 | private _useBigNumber: boolean 237 | 238 | /** 239 | * Create a Contract 240 | * 241 | * @param rpc - The RPC object used to access the blockchain. 242 | * @param info - The deployment information about this contract generated by 243 | * [solar](https://github.com/qtumproject/solar). It includes the contract 244 | * address, owner address, and ABI definition for methods and types. 245 | * @param opts - init options 246 | */ 247 | constructor( 248 | private rpc: QtumRPC, 249 | public info: IContractInfo, 250 | opts: IContractInitOptions = {}, 251 | ) { 252 | this.methodMap = new MethodMap(info.abi) 253 | this.address = info.address 254 | 255 | this._logDecoder = opts.logDecoder || new ContractLogDecoder(this.info.abi) 256 | 257 | this._useBigNumber = false 258 | } 259 | 260 | public encodeParams(method: string, args: any[] = []): string { 261 | const methodABI = this.methodMap.findMethod(method, args) 262 | if (!methodABI) { 263 | throw new Error(`Unknown method to call: ${method}`) 264 | } 265 | 266 | return encodeInputs(methodABI, args) 267 | } 268 | 269 | /** 270 | * Call a contract method using ABI encoding, and return the RPC result as is. 271 | * This does not create a transaction. It is useful for gas estimation or 272 | * getting results from read-only methods. 273 | * 274 | * @param method name of contract method to call 275 | * @param args arguments 276 | */ 277 | public async rawCall( 278 | method: string, 279 | args: any[] = [], 280 | opts: IContractCallRequestOptions = {}, 281 | ): Promise { 282 | log("call", method, args, opts) 283 | 284 | const calldata = this.encodeParams(method, args) 285 | 286 | return this.rpc.callContract({ 287 | address: this.address, 288 | datahex: calldata, 289 | senderAddress: opts.senderAddress || this.info.sender, 290 | ...opts, 291 | }) 292 | } 293 | 294 | /** 295 | * Executes contract method on your own local qtumd node as a "simulation" 296 | * using `callcontract`. It is free, and does not actually modify the 297 | * blockchain. 298 | * 299 | * @param method Name of the contract method 300 | * @param args Arguments for calling the method 301 | * @param opts call options 302 | */ 303 | public async call( 304 | method: string, 305 | args: any[] = [], 306 | opts: IContractCallRequestOptions = {}, 307 | ): Promise { 308 | const r = await this.rawCall(method, args, opts) 309 | 310 | // console.log("call result", r) 311 | 312 | const exception = r.executionResult.excepted 313 | if (exception !== "None") { 314 | throw new Error(`Call exception: ${exception}`) 315 | } 316 | 317 | const output = r.executionResult.output 318 | 319 | let decodedOutputs = [] 320 | if (output !== "") { 321 | const methodABI = this.methodMap.findMethod(method, args)! 322 | decodedOutputs = decodeOutputs(methodABI, output) 323 | } 324 | 325 | const decodedLogs = r.transactionReceipt.log.map((rawLog) => { 326 | return this.logDecoder.decode(rawLog) 327 | }) 328 | 329 | return Object.assign(r, { 330 | outputs: decodedOutputs, 331 | logs: decodedLogs, 332 | }) 333 | } 334 | 335 | /** 336 | * Call a method, and return only the first return value of the method. This 337 | * is a convenient syntatic sugar to get the return value when there is only 338 | * one. 339 | * 340 | * @param method Name of the contract method 341 | * @param args Arguments for calling the method 342 | * @param opts call options 343 | */ 344 | public async return( 345 | method: string, 346 | args: any[] = [], 347 | opts: IContractCallRequestOptions = {}, 348 | ): Promise { 349 | const result = await this.call(method, args, opts) 350 | return result.outputs[0] 351 | } 352 | 353 | public async returnNumber( 354 | method: string, 355 | args: any[] = [], 356 | opts: IContractCallRequestOptions = {}, 357 | ): Promise { 358 | const result = await this.call(method, args, opts) 359 | const val = result.outputs[0] 360 | 361 | // Convert big number to JavaScript number 362 | if (typeof val.toNumber !== "function") { 363 | throw new Error("Cannot convert result to a number") 364 | } 365 | 366 | return val.toNumber() 367 | } 368 | 369 | /** 370 | * Call a method, and return the first return value as Date. It is assumed 371 | * that the returned value is unix second. 372 | * 373 | * @param method 374 | * @param args 375 | * @param opts 376 | */ 377 | public async returnDate( 378 | method: string, 379 | args: any[] = [], 380 | opts: IContractCallRequestOptions = {}, 381 | ): Promise { 382 | const result = await this.return(method, args, opts) 383 | if (typeof result !== "number") { 384 | throw Error( 385 | "Cannot convert return value to Date. Expect return value to be a number.", 386 | ) 387 | } 388 | 389 | return new Date(result * 1000) 390 | } 391 | 392 | /** 393 | * Call a method, and return the first return value (a uint). Convert the value to 394 | * the desired currency unit. 395 | * 396 | * @param targetBase The currency unit to convert to. If a number, it is 397 | * treated as the power of 10. -8 is satoshi. 0 is the canonical unit. 398 | * @param method 399 | * @param args 400 | * @param opts 401 | */ 402 | public async returnCurrency( 403 | targetBase: number | string, 404 | method: string, 405 | args: any[] = [], 406 | opts: IContractCallRequestOptions = {}, 407 | ): Promise { 408 | const value = await this.return(method, args, opts) 409 | 410 | if (typeof value !== "number") { 411 | throw Error( 412 | "Cannot convert return value to currency unit. Expect return value to be a number.", 413 | ) 414 | } 415 | 416 | let base: number = 0 417 | 418 | if (typeof targetBase === "number") { 419 | base = targetBase 420 | } else { 421 | switch (targetBase) { 422 | case "qtum": 423 | case "btc": 424 | base = 0 425 | break 426 | case "sat": 427 | case "satoshi": 428 | base = -8 429 | default: 430 | throw Error(`Unknown base currency unit: ${targetBase}`) 431 | } 432 | } 433 | 434 | const satoshi = 1e-8 435 | 436 | return (value * satoshi) / 10 ** base 437 | } 438 | 439 | public async returnAs( 440 | converter: (val: any) => T | Promise, 441 | method: string, 442 | args: any[] = [], 443 | opts: IContractCallRequestOptions = {}, 444 | ): Promise { 445 | const value = await this.return(method, args, opts) 446 | return await converter(value) 447 | } 448 | 449 | /** 450 | * Create a transaction that calls a method using ABI encoding, and return the 451 | * RPC result as is. A transaction will require network consensus to confirm, 452 | * and costs you gas. 453 | * 454 | * @param method name of contract method to call 455 | * @param args arguments 456 | */ 457 | public async rawSend( 458 | method: string, 459 | args: any[], 460 | opts: IContractSendRequestOptions = {}, 461 | ): Promise { 462 | // TODO opts: gas limit, gas price, sender address 463 | const methodABI = this.methodMap.findMethod(method, args) 464 | if (methodABI == null) { 465 | throw new Error(`Unknown method to send: ${method}`) 466 | } 467 | 468 | if (methodABI.constant) { 469 | throw new Error(`cannot send to a constant method: ${method}`) 470 | } 471 | 472 | const calldata = encodeInputs(methodABI, args) 473 | 474 | return this.rpc.sendToContract({ 475 | address: this.address, 476 | datahex: calldata, 477 | senderAddress: opts.senderAddress || this.info.sender, 478 | ...opts, 479 | }) 480 | } 481 | 482 | /** 483 | * Confirms an in-wallet transaction, and return the receipt. 484 | * 485 | * @param txid transaction id. Must be an in-wallet transaction 486 | * @param confirm how many confirmations to ensure 487 | * @param onConfirm callback that receives the receipt for each additional confirmation 488 | */ 489 | public async confirm( 490 | txid: string, 491 | confirm?: number, 492 | onConfirm?: IContractSendConfirmationHandler, 493 | ): Promise { 494 | const txrp = new TxReceiptPromise(this.rpc, txid) 495 | 496 | if (onConfirm) { 497 | txrp.onConfirm((tx2, receipt2) => { 498 | const sendTxReceipt = this._makeSendTxReceipt(receipt2) 499 | onConfirm(tx2, sendTxReceipt) 500 | }) 501 | } 502 | 503 | const receipt = await txrp.confirm(confirm) 504 | 505 | return this._makeSendTxReceipt(receipt) 506 | } 507 | 508 | /** 509 | * Returns the receipt for a transaction, with decoded event logs. 510 | * 511 | * @param txid transaction id. Must be an in-wallet transaction 512 | * @returns The receipt, or null if transaction is not yet confirmed. 513 | */ 514 | public async receipt(txid: string): Promise { 515 | const receipt = await this.rpc.getTransactionReceipt({ txid }) 516 | if (!receipt) { 517 | return null 518 | } 519 | 520 | return this._makeSendTxReceipt(receipt) 521 | } 522 | 523 | public async send( 524 | method: string, 525 | args: any[] = [], 526 | opts: IContractSendRequestOptions = {}, 527 | ): Promise { 528 | log("send", method, args, opts) 529 | const methodABI = this.methodMap.findMethod(method, args) 530 | if (methodABI == null) { 531 | throw new Error(`Unknown method to send: ${method}`) 532 | } 533 | 534 | if (methodABI.constant) { 535 | throw new Error(`cannot send to a constant method: ${method}`) 536 | } 537 | 538 | const calldata = encodeInputs(methodABI, args) 539 | 540 | const sent = await this.rpc.sendToContract({ 541 | datahex: calldata, 542 | address: this.address, 543 | senderAddress: opts.senderAddress || this.info.sender, 544 | ...opts, 545 | }) 546 | 547 | const txid = sent.txid 548 | 549 | const txinfo = await this.rpc.getTransaction({ txid }) 550 | 551 | const sendTx = { 552 | ...txinfo, 553 | method, 554 | confirm: (n?: number, handler?: IContractSendConfirmationHandler) => { 555 | return this.confirm(txid, n, handler) 556 | }, 557 | } 558 | 559 | return sendTx 560 | } 561 | 562 | /** 563 | * Get contract event logs, up to the latest block. By default, it starts looking 564 | * for logs from the beginning of the blockchain. 565 | * @param req 566 | */ 567 | public async logs( 568 | req: IRPCWaitForLogsRequest = {}, 569 | ): Promise { 570 | return this.waitLogs({ 571 | fromBlock: 0, 572 | toBlock: "latest", 573 | ...req, 574 | }) 575 | } 576 | 577 | /** 578 | * Get contract event logs. Long-poll wait if no log is found. 579 | * @param req (optional) IRPCWaitForLogsRequest 580 | */ 581 | public async waitLogs( 582 | req: IRPCWaitForLogsRequest = {}, 583 | ): Promise { 584 | const filter = req.filter || {} 585 | if (!filter.addresses) { 586 | filter.addresses = [this.address] 587 | } 588 | 589 | const result = await this.rpc.waitforlogs({ 590 | ...req, 591 | filter, 592 | }) 593 | 594 | const entries = result.entries.map((entry) => { 595 | const parsedLog = this.logDecoder.decode(entry) 596 | return { 597 | ...entry, 598 | event: parsedLog, 599 | } 600 | }) 601 | 602 | return { 603 | ...result, 604 | entries, 605 | } 606 | } 607 | 608 | /** 609 | * Subscribe to contract's events, using callback interface. 610 | */ 611 | public onLog( 612 | fn: (entry: IContractEventLog) => void, 613 | opts: IRPCWaitForLogsRequest = {}, 614 | ) { 615 | let nextblock = opts.fromBlock || "latest" 616 | 617 | const loop = async () => { 618 | while (true) { 619 | const result = await this.waitLogs({ 620 | ...opts, 621 | fromBlock: nextblock, 622 | }) 623 | 624 | for (const entry of result.entries) { 625 | fn(entry) 626 | } 627 | 628 | nextblock = result.nextblock 629 | } 630 | } 631 | 632 | loop() 633 | } 634 | 635 | /** 636 | * Subscribe to contract's events, use EventsEmitter interface. 637 | */ 638 | public logEmitter(opts: IRPCWaitForLogsRequest = {}): EventEmitter { 639 | const emitter = new EventEmitter() 640 | 641 | this.onLog((entry) => { 642 | const key = (entry.event && entry.event.type) || "?" 643 | emitter.emit(key, entry) 644 | }, opts) 645 | 646 | return emitter 647 | } 648 | 649 | private get logDecoder(): ContractLogDecoder { 650 | return this._logDecoder 651 | } 652 | 653 | private _makeSendTxReceipt( 654 | receipt: IRPCGetTransactionReceiptResult, 655 | ): IContractSendReceipt { 656 | // https://stackoverflow.com/a/34710102 657 | // ...receiptNoLog will be a copy of receipt, without the `log` property 658 | const { log: rawlogs, ...receiptNoLog } = receipt 659 | const logs = rawlogs.map((rawLog) => this.logDecoder.decode(rawLog)!) 660 | 661 | return { 662 | ...receiptNoLog, 663 | logs, 664 | rawlogs, 665 | } 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /src/Contract_test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | import { assert } from "chai" 3 | 4 | import { repoData, rpc, assertThrow, generateBlock } from "./test" 5 | import { Contract } from "./Contract" 6 | 7 | describe("Contract", () => { 8 | // don't act as sender 9 | const { sender: _, ...info } = repoData.contracts[ 10 | "test/contracts/Methods.sol" 11 | ] 12 | 13 | const contract = new Contract(rpc, info) 14 | 15 | describe("#call", async () => { 16 | it("calls a method and get returned value", async () => { 17 | const result = await contract.call("getFoo") 18 | assert.hasAllKeys(result, [ 19 | "address", 20 | "executionResult", 21 | "transactionReceipt", 22 | "logs", 23 | "outputs", 24 | ]) 25 | 26 | const { outputs } = result 27 | 28 | assert.isArray(outputs) 29 | assert.isNumber(outputs[0].toNumber()) 30 | }) 31 | 32 | it("throws error if method doesn't exist", async () => { 33 | await assertThrow(async () => { 34 | await contract.call("unknownMethod") 35 | }) 36 | }) 37 | 38 | it("throws error if using invalid number of parameters for a method", async () => { 39 | await assertThrow(async () => { 40 | await contract.call("getFoo", [1]) 41 | }, "invalid number of parameters") 42 | }) 43 | 44 | it("throws error if using invalid type for a parameter", async () => { 45 | await assertThrow(async () => { 46 | await contract.call("setFoo", ["zfoo bar baz"]) 47 | }, "invalid parameter type") 48 | }) 49 | 50 | describe("method overloading", () => { 51 | const overload = new Contract( 52 | rpc, 53 | repoData.contracts["test/contracts/MethodOverloading.sol"], 54 | ) 55 | 56 | it("calls a method and get returned value", async () => { 57 | let result 58 | result = await overload.call("foo") 59 | assert.equal(result.outputs[0], "foo()") 60 | 61 | result = await overload.call("foo()") 62 | assert.equal(result.outputs[0], "foo()") 63 | 64 | result = await overload.call("foo(uint256)", [1]) 65 | assert.equal(result.outputs[0], "foo(uint256)") 66 | result = await overload.call("foo(string)", ["a"]) 67 | assert.equal(result.outputs[0], "foo(string)") 68 | 69 | result = await overload.call("foo(uint256,uint256)", [1, 2]) 70 | assert.equal(result.outputs[0], "foo(uint256,uint256)") 71 | result = await overload.call("foo(int256,int256)", [1, 2]) 72 | assert.equal(result.outputs[0], "foo(int256,int256)") 73 | 74 | result = await overload.call("foo", [1, 2, 3]) 75 | assert.equal(result.outputs[0], "foo(int256,int256,int256)") 76 | result = await overload.call("foo(int256,int256,int256)", [1, 2, 3]) 77 | assert.equal(result.outputs[0], "foo(int256,int256,int256)") 78 | }) 79 | }) 80 | }) 81 | 82 | describe("ABI encoding", async () => { 83 | it("can encode address[]", async () => { 84 | const logs = new Contract( 85 | rpc, 86 | repoData.contracts["test/contracts/ArrayArguments.sol"], 87 | ) 88 | 89 | const calldata = logs.encodeParams("takeArray", [ 90 | [ 91 | "aa00000000000000000000000000000000000011", 92 | "bb00000000000000000000000000000000000022", 93 | ], 94 | ]) 95 | 96 | assert.equal( 97 | calldata, 98 | // tslint:disable-next-line:max-line-length 99 | `ee3b88ea00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000aa00000000000000000000000000000000000011000000000000000000000000bb00000000000000000000000000000000000022`, 100 | ) 101 | }) 102 | }) 103 | 104 | describe("#send", async () => { 105 | it("can send and confirm tx", async () => { 106 | const v = Math.floor(Math.random() * 1000000) 107 | 108 | const tx = await contract.send("setFoo", [v]) 109 | 110 | assert.equal(tx.confirmations, 0) 111 | 112 | await generateBlock(1) 113 | 114 | const receipt = await tx.confirm(1, (r) => { 115 | assert.equal(r.confirmations, 1) 116 | }) 117 | 118 | assert.hasAllKeys(receipt, [ 119 | "blockHash", 120 | "blockNumber", 121 | "transactionHash", 122 | "transactionIndex", 123 | "from", 124 | "to", 125 | "excepted", 126 | "exceptedMessage", 127 | "cumulativeGasUsed", 128 | "gasUsed", 129 | "contractAddress", 130 | "logs", 131 | "outputIndex", 132 | "rawlogs", 133 | ]) 134 | 135 | const result = await contract.call("getFoo") 136 | assert.equal(result.outputs[0].toNumber(), v) 137 | }) 138 | 139 | it("throws error if method exists but is constant", async () => { 140 | await assertThrow(async () => { 141 | await contract.send("getFoo") 142 | }, "method is contant") 143 | }) 144 | }) 145 | 146 | describe("event logs", () => { 147 | const logs = new Contract( 148 | rpc, 149 | repoData.contracts["test/contracts/Logs.sol"], 150 | ) 151 | 152 | it("decodes logs for call", async () => { 153 | const result = await logs.call("emitFooEvent", ["abc"]) 154 | assert.deepEqual(result.logs, [ 155 | { 156 | type: "FooEvent", 157 | a: "abc", 158 | }, 159 | ]) 160 | }) 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /src/ContractsRepo.ts: -------------------------------------------------------------------------------- 1 | import { IContractInfo, Contract } from "./Contract" 2 | import { IABIMethod } from "./ethjs-abi" 3 | import { QtumRPC } from "./QtumRPC" 4 | import { ContractLogDecoder } from "./abi" 5 | import { EventListener } from "./EventListener" 6 | 7 | export interface IABIDefs { 8 | [key: string]: { 9 | abi: IABIMethod[] 10 | } 11 | } 12 | 13 | /** 14 | * Information about contracts 15 | */ 16 | export interface IContractsRepoData { 17 | /** 18 | * Information about deployed contracts 19 | */ 20 | contracts: { 21 | [key: string]: IContractInfo 22 | } 23 | 24 | /** 25 | * Information about deployed libraries 26 | */ 27 | libraries: { 28 | [key: string]: IContractInfo 29 | } 30 | 31 | /** 32 | * Information of contracts referenced by deployed contract/libraries, but not deployed 33 | */ 34 | related: { 35 | [key: string]: { 36 | abi: IABIMethod[] 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * ContractsRepo contains the ABI definitions of all known contracts 43 | */ 44 | export class ContractsRepo { 45 | /** 46 | * A logDecoder that knows about events defined in all known contracts. 47 | */ 48 | public logDecoder: ContractLogDecoder 49 | 50 | constructor(private rpc: QtumRPC, private repoData: IContractsRepoData) { 51 | const eventABIs = this.allEventABIs() 52 | this.logDecoder = new ContractLogDecoder(eventABIs) 53 | } 54 | 55 | public contract(name: string, info?: IContractInfo): Contract { 56 | 57 | if (info == null) { 58 | info = this.repoData.contracts[name] 59 | if (!info) { 60 | throw new Error(`cannot find contract: ${name}`) 61 | } 62 | } 63 | 64 | // Instantiate the contract with a log decoder that can handle all known events 65 | return new Contract(this.rpc, info, { logDecoder: this.logDecoder }) 66 | } 67 | 68 | public eventListener(): EventListener { 69 | return new EventListener(this.rpc, this.logDecoder) 70 | } 71 | 72 | /** 73 | * Combine all known event ABIs into one single array 74 | */ 75 | private allEventABIs(): IABIMethod[] { 76 | const allEventABIs: IABIMethod[] = [] 77 | 78 | const { contracts, libraries, related } = this.repoData 79 | 80 | if (contracts) { 81 | mergeDefs(contracts) 82 | } 83 | 84 | if (libraries) { 85 | mergeDefs(libraries) 86 | } 87 | 88 | if (related) { 89 | mergeDefs(related) 90 | } 91 | 92 | return allEventABIs 93 | 94 | // inner utility function for allEventABIs 95 | function mergeDefs(abiDefs: IABIDefs) { 96 | for (const key of Object.keys(abiDefs)) { 97 | const defs = abiDefs[key].abi 98 | 99 | for (const def of defs) { 100 | if (def.type === "event") { 101 | allEventABIs.push(def) 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/ContractsRepo_test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | import { assert } from "chai" 3 | 4 | import { repoData, rpc, assertThrow } from "./test" 5 | import { ContractsRepo } from "./ContractsRepo" 6 | 7 | describe("ContractsRepo", () => { 8 | // don't act as sender 9 | const repo = new ContractsRepo(rpc, repoData) 10 | 11 | it("can instantiate a contract", () => { 12 | const contract = repo.contract("test/contracts/LogOfDependantContract.sol") 13 | 14 | assert.isNotNull(contract) 15 | assert.strictEqual( 16 | contract.info, 17 | repoData.contracts["test/contracts/LogOfDependantContract.sol"], 18 | ) 19 | }) 20 | 21 | it("can instantiate a contract with an log decoder that knows about all events", async () => { 22 | const contract = repo.contract("test/contracts/LogOfDependantContract.sol") 23 | 24 | const result = await contract.call("emitLog") 25 | 26 | const fooEvent = result.logs[0]! 27 | 28 | assert.isNotNull(fooEvent) 29 | assert.deepEqual(fooEvent[0], "Foo!") 30 | assert.deepEqual(fooEvent, { 31 | data: "Foo!", 32 | type: "LogOfDependantContractChildEvent", 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/EventListener.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3" 2 | 3 | import { QtumRPC, IRPCWaitForLogsRequest, IPromiseCancel } from "./QtumRPC" 4 | import { ContractLogDecoder } from "./abi" 5 | import { IContractEventLogs, IContractEventLog } from "./Contract" 6 | 7 | export type ICancelFunction = () => void 8 | 9 | export interface ICancellableEventEmitter extends EventEmitter { 10 | cancel: ICancelFunction 11 | } 12 | 13 | export class EventListener { 14 | // TODO filter out unparseable logs 15 | 16 | constructor(private rpc: QtumRPC, private logDecoder: ContractLogDecoder) {} 17 | 18 | /** 19 | * Get contract event logs. Long-poll wait if no log is found. Returns a cancel 20 | * function that stops the events subscription. 21 | * 22 | * @param req (optional) IRPCWaitForLogsRequest 23 | */ 24 | public waitLogs( 25 | req: IRPCWaitForLogsRequest = {}, 26 | ): IPromiseCancel { 27 | const filter = req.filter || {} 28 | 29 | const logPromise = this.rpc.waitforlogs({ 30 | ...req, 31 | filter, 32 | }) 33 | 34 | return logPromise.then((result) => { 35 | const entries = result.entries.map((entry) => { 36 | const parsedLog = this.logDecoder.decode(entry) 37 | return { 38 | ...entry, 39 | event: parsedLog, 40 | } 41 | }) 42 | 43 | return { 44 | ...result, 45 | entries, 46 | } 47 | }) as any // bypass typechecker problem 48 | } 49 | 50 | /** 51 | * Subscribe to contract's events, using callback interface. 52 | */ 53 | public onLog( 54 | fn: (entry: IContractEventLog) => void, 55 | opts: IRPCWaitForLogsRequest = {}, 56 | ): ICancelFunction { 57 | let nextblock = opts.fromBlock || "latest" 58 | 59 | let promiseCancel: () => void 60 | let canceled = false 61 | 62 | const asyncLoop = async () => { 63 | while (true) { 64 | if (canceled) { 65 | break 66 | } 67 | 68 | const logPromise = this.waitLogs({ 69 | ...opts, 70 | fromBlock: nextblock, 71 | }) 72 | 73 | promiseCancel = logPromise.cancel 74 | 75 | const result = await logPromise 76 | 77 | for (const entry of result.entries) { 78 | fn(entry) 79 | } 80 | 81 | nextblock = result.nextblock 82 | } 83 | } 84 | 85 | asyncLoop() 86 | 87 | // return a cancel function 88 | return () => { 89 | canceled = true 90 | if (promiseCancel) { 91 | promiseCancel() 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Subscribe to contract's events, use EventsEmitter interface. 98 | */ 99 | public emitter(opts: IRPCWaitForLogsRequest = {}): ICancellableEventEmitter { 100 | const emitter = new EventEmitter() 101 | 102 | const cancel = this.onLog((entry) => { 103 | const key = (entry.event && entry.event.type) || "?" 104 | emitter.emit(key, entry) 105 | }, opts) 106 | 107 | return Object.assign(emitter, { 108 | cancel, 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/EventListener_test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | import { assert } from "chai" 3 | 4 | import { repoData, rpc, generateBlock, assertThrow } from "./test" 5 | import { ContractsRepo } from "./ContractsRepo" 6 | import { IContractInfo } from "./Contract" 7 | 8 | describe("EventListener", () => { 9 | // don't act as sender 10 | const repo = new ContractsRepo(rpc, repoData) 11 | 12 | // it("can decode events emitted by any known contract", async () => { 13 | // const listener = repo.eventListener() 14 | 15 | // const contract = repo.contract("test/contracts/LogOfDependantContract.sol") 16 | // const logPromise = listener.waitLogs({ minconf: 0 }) 17 | 18 | // const tx = await contract.send("emitLog") 19 | 20 | // generateBlock() 21 | 22 | // const logs = await logPromise 23 | // // console.log("logs", JSON.stringify(logs, null, 2)) 24 | 25 | // const fooEvent = logs.entries[0] 26 | 27 | // assert.isNotNull(fooEvent.event) 28 | // assert.deepEqual(fooEvent.event, { 29 | // data: "Foo!", 30 | // type: "LogOfDependantContractChildEvent", 31 | // }) 32 | // }) 33 | 34 | describe("#onLog", () => { 35 | const contract = repo.contract("test/contracts/Logs.sol") 36 | const listener = repo.eventListener() 37 | 38 | it("can receive a log using callback", (done) => { 39 | contract.send("emitFooEvent", ["test!"]) 40 | 41 | const cancelOnLog = listener.onLog( 42 | (entry) => { 43 | const fooEvent = entry.event! 44 | assert.deepEqual(fooEvent, { a: "test!", type: "FooEvent" }) 45 | cancelOnLog() 46 | done() 47 | }, 48 | { minconf: 0 }, 49 | ) 50 | 51 | generateBlock() 52 | }) 53 | }) 54 | 55 | // it("should leave unrecognized events unparsed", async () => { 56 | // const logContractInfo: IContractInfo = 57 | // repoData.contracts["test/contracts/Logs.sol"] 58 | 59 | // logContractInfo.abi = logContractInfo.abi.filter( 60 | // (def) => !Object.is(def.name, "BazEvent"), 61 | // ) 62 | 63 | // const repoData2 = { 64 | // contracts: { Logs: logContractInfo }, 65 | // libraries: {}, 66 | // related: {}, 67 | // } 68 | 69 | // const repo2 = new ContractsRepo(rpc, repoData2) 70 | 71 | // const logContract = repo2.contract("Logs") 72 | 73 | // const listener = repo2.eventListener() 74 | 75 | // const logPromise = listener.waitLogs({ minconf: 0 }) 76 | // const tx = logContract.send("emitMultipleEvents", ["test!"]) 77 | // generateBlock() 78 | 79 | // const logs = await logPromise 80 | // // find unrecognized BazEvent, whose topic is BazEvent 81 | // const bazEvent = logs.entries.find((entry) => 82 | // Object.is( 83 | // entry.topics[0], 84 | // "ebe3309556157bcfc1c4e8912c38f6994609d30dc7f5fa520622bf176b9bcec3", 85 | // ), 86 | // )! 87 | 88 | // assert.equal(logs.count, 3) 89 | // assert.isNotNull(bazEvent) 90 | // assert.isNull(bazEvent.event) 91 | 92 | // // console.log("logs", JSON.stringify(logs, null, 2)) 93 | // }) 94 | 95 | describe("#onLog", () => { 96 | const contract = repo.contract("test/contracts/Logs.sol") 97 | const listener = repo.eventListener() 98 | 99 | it("can receive a log using callback", (done) => { 100 | contract.send("emitFooEvent", ["test!"]) 101 | 102 | const cancelOnLog = listener.onLog( 103 | (entry) => { 104 | const fooEvent = entry.event! 105 | assert.deepEqual(fooEvent, { a: "test!", type: "FooEvent" }) 106 | 107 | // clean up test by unsubscribing from events 108 | cancelOnLog() 109 | done() 110 | }, 111 | { minconf: 0 }, 112 | ) 113 | 114 | generateBlock() 115 | }) 116 | }) 117 | 118 | // describe("#emitter", () => { 119 | // const contract = repo.contract("test/contracts/Logs.sol") 120 | // const listener = repo.eventListener() 121 | 122 | // it("can receive logs using event emitter", (done) => { 123 | // contract.send("emitFooEvent", ["test!"]) 124 | 125 | // const emitter = listener.emitter({ minconf: 0 }) 126 | // emitter.on("FooEvent", (entry) => { 127 | // const fooEvent = entry.event! 128 | 129 | // assert.deepEqual(fooEvent, { a: "test!", type: "FooEvent" }) 130 | 131 | // // clean up test by unsubscribing from events 132 | // emitter.cancel() 133 | // done() 134 | // }) 135 | 136 | // generateBlock() 137 | // }) 138 | // }) 139 | // TODO can listen for specific topic 140 | }) 141 | -------------------------------------------------------------------------------- /src/MethodMap.ts: -------------------------------------------------------------------------------- 1 | import { IABIMethod } from "./index" 2 | 3 | /** 4 | * Build an index of a contract's ABI definitions. 5 | */ 6 | export class MethodMap { 7 | private methods: { [key: string]: IABIMethod } = {} 8 | 9 | constructor(_methods: IABIMethod[]) { 10 | const keyCollisions: Set = new Set() 11 | 12 | for (const method of _methods) { 13 | if (method.type !== "function") { 14 | continue 15 | } 16 | 17 | const key = `${method.name}#${method.inputs.length}` 18 | 19 | const sig = `${method.name}(${method.inputs 20 | .map((input) => input.type) 21 | .join(",")})` 22 | 23 | if (this.methods[key]) { 24 | // Detected ambiguity for this arity. User must use method signature 25 | // to select the method. 26 | keyCollisions.add(key) 27 | } else { 28 | this.methods[key] = method 29 | } 30 | 31 | this.methods[sig] = method 32 | } 33 | 34 | for (const key of keyCollisions) { 35 | delete this.methods[key] 36 | } 37 | } 38 | 39 | /** 40 | * Solidity allows method name overloading. If there's no ambiguity, allow 41 | * the name of the method as selector. If there is ambiguity (same number 42 | * of arguments, different types), must use the method signature. 43 | * 44 | * Example: 45 | * 46 | * foo(uint a, uint b) 47 | * 48 | * The method name is `foo`. 49 | * The method signature is `foo(uint, uint)` 50 | */ 51 | public findMethod( 52 | selector: string, 53 | args: any[] = [], 54 | ): IABIMethod | undefined { 55 | // Find method by method signature 56 | const method = this.methods[selector] 57 | if (method) { 58 | return method 59 | } 60 | 61 | // Find method by method name 62 | const key = `${selector}#${args.length}` 63 | 64 | return this.methods[key] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/MethodMap_test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | import { assert } from "chai" 3 | 4 | import { repoData, rpc, assertThrow } from "./test" 5 | import { Contract } from "./Contract" 6 | import { MethodMap } from "./MethodMap" 7 | 8 | describe("MethodMap", () => { 9 | // don't act as sender 10 | const methods = repoData.contracts["test/contracts/MethodOverloading.sol"].abi 11 | 12 | const map = new MethodMap(methods) 13 | 14 | it("can find method by name, if no ambiguity", () => { 15 | const method = map.findMethod("foo", [])! 16 | 17 | assert.deepEqual(method, { 18 | name: "foo", 19 | type: "function", 20 | payable: false, 21 | inputs: [], 22 | outputs: [{ name: "", type: "string", indexed: false }], 23 | constant: false, 24 | anonymous: false, 25 | }) 26 | // console.log(method) 27 | }) 28 | 29 | it("can find method by method signature", () => { 30 | const method = map.findMethod("foo()", []) 31 | 32 | assert.deepEqual(method, { 33 | name: "foo", 34 | type: "function", 35 | payable: false, 36 | inputs: [], 37 | outputs: [{ name: "", type: "string", indexed: false }], 38 | constant: false, 39 | anonymous: false, 40 | }) 41 | }) 42 | 43 | it("can disambiguate method of same arity by signature", () => { 44 | let method = map.findMethod("foo(string)") 45 | 46 | assert.deepEqual(method, { 47 | name: "foo", 48 | type: "function", 49 | payable: false, 50 | inputs: [{ name: "_a", type: "string", indexed: false }], 51 | outputs: [{ name: "", type: "string", indexed: false }], 52 | constant: false, 53 | anonymous: false, 54 | }) 55 | 56 | method = map.findMethod("foo(uint256)") 57 | 58 | assert.deepEqual(method, { 59 | name: "foo", 60 | type: "function", 61 | payable: false, 62 | inputs: [{ name: "_a", type: "uint256", indexed: false }], 63 | outputs: [{ name: "", type: "string", indexed: false }], 64 | constant: false, 65 | anonymous: false, 66 | }) 67 | }) 68 | 69 | it("cannot find method by name, if there is ambiguity", () => { 70 | let method = map.findMethod("foo", [1]) 71 | assert.isUndefined(method) 72 | 73 | method = map.findMethod("foo", [1, 2]) 74 | assert.isUndefined(method) 75 | }) 76 | 77 | it("can disambiguate method by number of parameters", () => { 78 | const method = map.findMethod("foo", [1, 2, 3]) 79 | 80 | assert.deepEqual(method, { 81 | name: "foo", 82 | type: "function", 83 | payable: false, 84 | inputs: [ 85 | { name: "_a", type: "int256", indexed: false }, 86 | { name: "_b", type: "int256", indexed: false }, 87 | { name: "_c", type: "int256", indexed: false }, 88 | ], 89 | outputs: [{ name: "", type: "string", indexed: false }], 90 | constant: false, 91 | anonymous: false, 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /src/Qtum.ts: -------------------------------------------------------------------------------- 1 | import { QtumRPC } from "./QtumRPC" 2 | import { IContractsRepoData, ContractsRepo } from "./ContractsRepo" 3 | import { Contract, IContractInfo } from "./Contract" 4 | 5 | /** 6 | * The `Qtum` class is an instance of the `qtumjs` API. 7 | * 8 | * @param providerURL URL of the qtumd RPC service. 9 | * @param repoData Information about Solidity contracts. 10 | */ 11 | export class Qtum extends QtumRPC { 12 | private repo: ContractsRepo 13 | 14 | constructor(providerURL: string, repoData?: IContractsRepoData) { 15 | super(providerURL) 16 | this.repo = new ContractsRepo(this, { 17 | // massage the repoData by providing empty default properties 18 | contracts: {}, 19 | libraries: {}, 20 | related: {}, 21 | ...repoData, 22 | }) 23 | } 24 | 25 | /** 26 | * A factory method to instantiate a `Contract` instance using the ABI 27 | * definitions and address found in `repoData`. The Contract instance is 28 | * configured with an event log decoder that can decode all known event types 29 | * found in `repoData`. 30 | * 31 | * @param name The name of a deployed contract 32 | */ 33 | public contract(name: string, info?: IContractInfo): Contract { 34 | return this.repo.contract(name, info) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/QtumRPC.ts: -------------------------------------------------------------------------------- 1 | import { QtumRPCRaw } from "./QtumRPCRaw" 2 | import { Hash } from "crypto" 3 | 4 | 5 | // { 6 | // chain: 'regtest', 7 | // blocks: 1876, 8 | // headers: 1876, 9 | // bestblockhash: '65c1656096558d6d1f8a1ee23c6adecc1be528d1fc2db162851a7cacb4bb5c14', 10 | // difficulty: 4.656542373906925e-10, 11 | // moneysupply: 37520000, 12 | // mediantime: 1597917184, 13 | // verificationprogress: 1, 14 | // initialblockdownload: false, 15 | // chainwork: '0000000000000000000000000000000000000000000000000000000000000eaa', 16 | // size_on_disk: 812854, 17 | // pruned: false, 18 | // softforks: { 19 | // bip34: { type: 'buried', active: true, height: 0 }, 20 | // bip66: { type: 'buried', active: true, height: 0 }, 21 | // bip65: { type: 'buried', active: true, height: 0 }, 22 | // csv: { type: 'buried', active: true, height: 432 }, 23 | // segwit: { type: 'buried', active: true, height: 0 }, 24 | // testdummy: { type: 'bip9', bip9: [Object], height: 432, active: true } 25 | // }, 26 | // warnings: '' 27 | // } 28 | 29 | export interface IGetBlockChainInfoResult { 30 | chain: string 31 | blocks: number 32 | headers: number 33 | bestblockhash: string 34 | difficulty: number 35 | moneysupply: number 36 | mediantime: number 37 | verificationprogress: number 38 | initialblockdownload: boolean 39 | chainwork: string 40 | size_on_disk: number 41 | pruned: boolean 42 | softforks: any 43 | warnings: string 44 | } 45 | 46 | export interface IRPCSendToContractRequest { 47 | /** 48 | * (required) The contract address that will receive the funds and data. 49 | */ 50 | address: string 51 | 52 | /** 53 | * (required) data to send 54 | */ 55 | datahex: string 56 | 57 | /** 58 | * The amount in QTUM to send. eg 0.1, default: 0 59 | */ 60 | amount?: number | string 61 | 62 | /** 63 | * gasLimit, default: 200000, max: 40000000 64 | */ 65 | gasLimit?: number 66 | 67 | /** 68 | * Qtum price per gas unit, default: 0.00000001, min:0.00000001 69 | */ 70 | gasPrice?: number | string 71 | 72 | /** 73 | * The quantum address that will be used as sender. 74 | */ 75 | senderAddress?: string 76 | 77 | /** 78 | * Whether to broadcast the transaction or not (default: true) 79 | */ 80 | // broadcast?: boolean 81 | } 82 | 83 | export interface IRPCSendToContractResult { 84 | /** 85 | * The transaction id. 86 | */ 87 | txid: string 88 | /** 89 | * QTUM address of the sender. 90 | */ 91 | sender: string 92 | /** 93 | * ripemd-160 hash of the sender. 94 | */ 95 | hash160: string 96 | } 97 | 98 | export interface IRPCCallContractRequest { 99 | /** 100 | * (required) The account address 101 | */ 102 | address: string 103 | 104 | /** 105 | * (required) The data hex string 106 | */ 107 | datahex: string 108 | 109 | /** 110 | * The sender address hex string 111 | */ 112 | senderAddress?: string 113 | 114 | gasLimit?: number 115 | amount?: number 116 | } 117 | 118 | export interface IExecutionResult { 119 | gasUsed: number 120 | excepted: string 121 | newAddress: string 122 | output: string 123 | codeDeposit: number 124 | gasRefunded: number 125 | depositSize: number 126 | gasForDeposit: number 127 | } 128 | 129 | export interface IRPCCallContractResult { 130 | address: string 131 | executionResult: IExecutionResult 132 | transactionReceipt: { 133 | stateRoot: string 134 | gasUsed: string 135 | bloom: string 136 | 137 | // FIXME: Need better typing 138 | log: any[] 139 | } 140 | } 141 | 142 | export interface IRPCGetTransactionRequest { 143 | /** 144 | * The transaction id 145 | */ 146 | txid: string 147 | 148 | /** 149 | * (optional, default=false) Whether to include watch-only addresses in balance calculation and details[] 150 | */ 151 | include_watchonly?: boolean 152 | 153 | /** 154 | * (boolean, optional, default=false) Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decodera 155 | wtransaction) 156 | */ 157 | verbose?: boolean 158 | 159 | /** 160 | * (optional, default=0) Wait for enough confirmations before returning 161 | */ 162 | waitconf?: number 163 | } 164 | 165 | /** 166 | * Basic information about a transaction submitted to the network. 167 | */ 168 | export interface IRPCGetTransactionResult { 169 | amount: number 170 | fee: number 171 | confirmations: number 172 | blockhash: string 173 | blockindex: number 174 | blocktime: number 175 | txid: string 176 | // FIXME: Need better typing 177 | walletconflicts: any[] 178 | time: number 179 | timereceived: number 180 | "bip125-replaceable": "no" | "yes" | "unknown" 181 | // FIXME: Need better typing 182 | details: any[] 183 | hex: string 184 | } 185 | 186 | export interface IRPCGetTransactionReceiptRequest { 187 | /** 188 | * The transaction id 189 | */ 190 | txid: string 191 | } 192 | 193 | /** 194 | * Transaction receipt returned by qtumd 195 | */ 196 | export interface IRPCGetTransactionReceiptBase { 197 | blockHash: string 198 | blockNumber: number 199 | 200 | transactionHash: string 201 | transactionIndex: number 202 | 203 | from: string 204 | to: string 205 | 206 | cumulativeGasUsed: number 207 | gasUsed: number 208 | 209 | contractAddress: string 210 | } 211 | 212 | export interface IRPCGetTransactionReceiptResult 213 | extends IRPCGetTransactionReceiptBase { 214 | log: ITransactionLog[] 215 | } 216 | 217 | export interface ITransactionLog { 218 | address: string 219 | topics: string[] 220 | data: string 221 | } 222 | 223 | const sendToContractRequestDefaults = { 224 | amount: 0, 225 | gasLimit: 200000, 226 | // FIXME: Does not support string gasPrice although the doc says it does. 227 | gasPrice: 0.0000004, 228 | } 229 | 230 | export interface IRPCWaitForLogsRequest { 231 | /** 232 | * The block number to start looking for logs. 233 | */ 234 | fromBlock?: number | "latest" 235 | 236 | /** 237 | * The block number to stop looking for logs. If null, will wait indefinitely into the future. 238 | */ 239 | toBlock?: number | "latest" 240 | 241 | /** 242 | * Filter conditions for logs. Addresses and topics are specified as array of hexadecimal strings 243 | */ 244 | filter?: ILogFilter 245 | 246 | /** 247 | * Minimal number of confirmations before a log is returned 248 | */ 249 | minconf?: number 250 | } 251 | 252 | export interface ILogFilter { 253 | addresses?: string[] 254 | topics?: (string | null)[] 255 | } 256 | 257 | /** 258 | * The raw log data returned by qtumd, not ABI decoded. 259 | */ 260 | export interface ILogEntry extends IRPCGetTransactionReceiptBase { 261 | /** 262 | * EVM log topics 263 | */ 264 | topics: string[] 265 | 266 | /** 267 | * EVM log data, as hexadecimal string 268 | */ 269 | data: string 270 | } 271 | 272 | export interface IRPCWaitForLogsResult { 273 | entries: ILogEntry[] 274 | count: number 275 | nextblock: number 276 | } 277 | 278 | export interface IRPCSearchLogsRequest { 279 | /** 280 | * The number of the earliest block (latest may be given to mean the most recent block). 281 | * (default = "latest") 282 | */ 283 | fromBlock?: number | "latest" 284 | 285 | /** 286 | * The number of the latest block (-1 may be given to mean the most recent block). 287 | * (default = -1) 288 | */ 289 | toBlock?: number 290 | 291 | /** 292 | * An address or a list of addresses to only get logs from particular account(s). 293 | */ 294 | addresses?: string[] 295 | 296 | /** 297 | * An array of values which must each appear in the log entries. 298 | * The order is important, if you want to leave topics out use null, e.g. ["null", "0x00..."]. 299 | */ 300 | topics?: (string | null)[] 301 | 302 | /** 303 | * Minimal number of confirmations before a log is returned 304 | * (default = 0) 305 | */ 306 | minconf?: number 307 | } 308 | 309 | export type IRPCSearchLogsResult = IRPCGetTransactionReceiptResult[] 310 | 311 | export interface IPromiseCancel extends Promise { 312 | cancel: () => void 313 | } 314 | 315 | export class QtumRPC extends QtumRPCRaw { 316 | private _hasTxWaitSupport: boolean | undefined 317 | 318 | public getBlockChainInfo(): Promise { 319 | return this.rawCall("getblockchaininfo") 320 | } 321 | 322 | public sendToContract( 323 | req: IRPCSendToContractRequest, 324 | ): Promise { 325 | const vals = { 326 | ...sendToContractRequestDefaults, 327 | ...req, 328 | } 329 | 330 | const args = [ 331 | vals.address, 332 | vals.datahex, 333 | vals.amount, 334 | vals.gasLimit, 335 | vals.gasPrice, 336 | ] 337 | 338 | if (vals.senderAddress) { 339 | args.push(vals.senderAddress) 340 | } 341 | 342 | return this.rawCall("sendtocontract", args) 343 | } 344 | 345 | public callContract( 346 | req: IRPCCallContractRequest, 347 | ): Promise { 348 | const args: any[] = [req.address, req.datahex] 349 | 350 | args.push(req.senderAddress || "") 351 | args.push(req.gasLimit || 0) 352 | args.push(req.amount || 0) 353 | 354 | return this.rawCall("callcontract", args) 355 | } 356 | 357 | public getTransaction( 358 | req: IRPCGetTransactionRequest, 359 | ): Promise { 360 | const args: any[] = [req.txid] 361 | 362 | if (req.include_watchonly) { 363 | args.push(req.include_watchonly) 364 | } else { 365 | args.push(false) 366 | } 367 | 368 | if (req.verbose) { 369 | args.push(!!req.verbose) 370 | } else { 371 | args.push(false) 372 | } 373 | 374 | if (req.waitconf) { 375 | args.push(req.waitconf) 376 | } 377 | 378 | return this.rawCall("gettransaction", args) 379 | } 380 | 381 | public async getTransactionReceipt( 382 | req: IRPCGetTransactionRequest, 383 | ): Promise { 384 | // The raw RPC API returns [] if tx id doesn't exist or not mined yet 385 | // When transaction is mined, the API returns [receipt] 386 | // 387 | // We'll do the unwrapping here. 388 | const result: IRPCGetTransactionReceiptResult[] = await this.rawCall( 389 | "gettransactionreceipt", 390 | [req.txid], 391 | ) 392 | 393 | if (result.length === 0) { 394 | return null 395 | } 396 | 397 | return result[0] 398 | } 399 | 400 | /** 401 | * Long-poll request to get logs. Cancel the returned promise to terminate polling early. 402 | */ 403 | public waitforlogs( 404 | req: IRPCWaitForLogsRequest = {}, 405 | ): IPromiseCancel { 406 | const args = [req.fromBlock, req.toBlock, req.filter, req.minconf] 407 | 408 | const cancelTokenSource = this.cancelTokenSource() 409 | 410 | const p = this.rawCall("waitforlogs", args, { 411 | cancelToken: cancelTokenSource.token, 412 | }) 413 | 414 | return Object.assign(p, { 415 | cancel: cancelTokenSource.cancel.bind(cancelTokenSource), 416 | }) as any 417 | } 418 | 419 | public async searchlogs( 420 | _req: IRPCSearchLogsRequest = {}, 421 | ): Promise { 422 | const searchlogsDefaults = { 423 | fromBlock: "latest", 424 | toBlock: -1, 425 | addresses: [], 426 | topics: [], 427 | minconf: 0, 428 | } 429 | 430 | const req = { 431 | searchlogsDefaults, 432 | ..._req, 433 | } 434 | 435 | const args = [ 436 | req.fromBlock, 437 | req.toBlock, 438 | req.addresses, 439 | req.topics, 440 | req.minconf, 441 | ] 442 | 443 | return this.rawCall("searchlogs", args) 444 | } 445 | 446 | public async checkTransactionWaitSupport(): Promise { 447 | if (this._hasTxWaitSupport !== undefined) { 448 | return this._hasTxWaitSupport 449 | } 450 | 451 | const helpmsg: string = await this.rawCall("help", ["gettransaction"]) 452 | this._hasTxWaitSupport = helpmsg.split("\n")[0].indexOf("waitconf") !== -1 453 | return this._hasTxWaitSupport 454 | } 455 | 456 | public async fromHexAddress(hexAddress: string): Promise { 457 | return this.rawCall("fromhexaddress", [hexAddress]) 458 | } 459 | 460 | public async getHexAddress(address: string): Promise { 461 | return this.rawCall("gethexaddress", [address]) 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /src/QtumRPCRaw.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosInstance, 3 | AxiosPromise, 4 | AxiosRequestConfig, 5 | CancelToken, 6 | CancelTokenSource, 7 | } from "axios" 8 | const URL = require("url-parse") 9 | 10 | import debug from "debug" 11 | 12 | const log = debug("qtumjs:rpc") 13 | 14 | import { sleep } from "./sleep" 15 | 16 | export interface IJSONRPCRequest { 17 | id: any 18 | method: string 19 | params: any[] 20 | auth?: string 21 | } 22 | 23 | export interface IAuthorization { 24 | id: string 25 | state: "pending" | "accepted" | "denied" | "consumed" 26 | request: IJSONRPCRequest 27 | createdAt: string 28 | } 29 | 30 | export interface IRPCCallOption { 31 | cancelToken?: CancelToken 32 | } 33 | 34 | export class QtumRPCRaw { 35 | private idNonce: number 36 | private _api: AxiosInstance 37 | 38 | constructor(private _baseURL: string) { 39 | this.idNonce = 0 40 | 41 | const url = new URL(_baseURL) 42 | 43 | const config: AxiosRequestConfig = { 44 | baseURL: url.origin, 45 | // don't throw on non-200 response 46 | validateStatus: () => true, 47 | } 48 | 49 | if (url.username !== "" && url.password !== "") { 50 | config.auth = { 51 | username: url.username, 52 | password: url.password, 53 | } 54 | } 55 | 56 | this._api = axios.create(config) 57 | } 58 | 59 | public cancelTokenSource(): CancelTokenSource { 60 | return axios.CancelToken.source() 61 | } 62 | 63 | public async rawCall( 64 | method: string, 65 | params: any[] = [], 66 | opts: IRPCCallOption = {}, 67 | ): Promise { 68 | const rpcCall: IJSONRPCRequest = { 69 | method, 70 | params, 71 | id: this.idNonce++, 72 | } 73 | 74 | log("%O", { 75 | method, 76 | params, 77 | }) 78 | 79 | let res = await this.makeRPCCall(rpcCall) 80 | 81 | if (res.status === 402) { 82 | const auth: IAuthorization = res.data 83 | res = await this.authCall(auth.id, rpcCall) 84 | } 85 | 86 | if (res.status === 401) { 87 | // body is empty 88 | throw new Error(await res.statusText) 89 | } 90 | 91 | // 404 if method doesn't exist 92 | if (res.status === 404) { 93 | throw new Error(`unknown method: ${method}`) 94 | } 95 | 96 | if (res.status !== 200) { 97 | if (res.headers["content-type"] !== "application/json") { 98 | const body = await res.data 99 | throw new Error(`${res.status} ${res.statusText}\n${res.data}`) 100 | } 101 | 102 | const eresult = await res.data 103 | 104 | if (eresult.error) { 105 | const { code, message } = eresult.error 106 | throw new Error(`RPC Error: [${code}] ${message}`) 107 | } else { 108 | throw new Error(String(eresult)) 109 | } 110 | } 111 | 112 | const { result } = await res.data 113 | return result 114 | } 115 | 116 | private makeRPCCall(rpcCall: IJSONRPCRequest): AxiosPromise { 117 | return this._api.post("/", rpcCall) 118 | } 119 | 120 | private async authCall( 121 | authID: string, 122 | rpcCall: IJSONRPCRequest, 123 | ): Promise { 124 | // long-poll an authorization until its state changes 125 | const res = await this._api.get(`/api/authorizations/${authID}/onchange`) 126 | 127 | const { data } = res 128 | 129 | if (res.status !== 200) { 130 | throw new Error(data.message) 131 | } 132 | 133 | const auth: IAuthorization = data 134 | 135 | if (auth.state === "denied") { 136 | throw new Error(`Authorization denied: ${authID}`) 137 | } 138 | 139 | if (auth.state === "accepted") { 140 | return this.makeRPCCall({ 141 | ...rpcCall, 142 | auth: auth.id, 143 | }) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/QtumRPC_test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | 3 | import { assert } from "chai" 4 | 5 | import { rpc, assertThrow } from "./test" 6 | 7 | // import { } from "mocha" 8 | describe("QtumRPC", () => { 9 | it("can make RPC call", async () => { 10 | const info = await rpc.rawCall("getblockchaininfo") 11 | assert.isNotEmpty(info) 12 | // assert.hasAllKeys(info, [ 13 | // "version", 14 | // "protocolversion", 15 | // "walletversion", 16 | // "balance", 17 | // "stake", 18 | // "blocks", 19 | // "deprecation-warning", 20 | // "timeoffset", 21 | // "connections", 22 | // "proxy", 23 | // "difficulty", 24 | // "testnet", 25 | // "moneysupply", 26 | // "keypoololdest", 27 | // "keypoolsize", 28 | // "paytxfee", 29 | // "relayfee", 30 | // "errors", 31 | // ]) 32 | }) 33 | 34 | it("throws error if method is not found", async () => { 35 | await assertThrow(async () => { 36 | return rpc.rawCall("unknown-method") 37 | }) 38 | }) 39 | 40 | it("throws error if calling method using invalid params", async () => { 41 | await assertThrow(async () => { 42 | return rpc.rawCall("getinfo", [1, 2]) 43 | }) 44 | }) 45 | 46 | it("can convert a hex address to a p2pkh address", async () => { 47 | const p2pkhAddress = await rpc.fromHexAddress( 48 | "b22cbfd8dffcd4e0120279c2cc41315fac2335e2", 49 | ) 50 | assert.strictEqual(p2pkhAddress, "qZoV3RKeHaxKM5RnuZdA5bwoYTCH73QLrE") 51 | }) 52 | 53 | it("can convert a p2pkh address to a hex address", async () => { 54 | const hexAddress = await rpc.getHexAddress( 55 | "qZoV3RKeHaxKM5RnuZdA5bwoYTCH73QLrE", 56 | ) 57 | assert.strictEqual(hexAddress, "b22cbfd8dffcd4e0120279c2cc41315fac2335e2") 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/Qtum_test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | 3 | import { assert } from "chai" 4 | 5 | import { rpcURL, repoData } from "./test" 6 | import { Qtum } from "./Qtum" 7 | import { Contract } from "./Contract" 8 | 9 | describe("Qtum", () => { 10 | const qtum = new Qtum(rpcURL, repoData) 11 | 12 | it("can instantiate a contract", () => { 13 | const contract = qtum.contract("test/contracts/Methods.sol") 14 | assert.instanceOf(contract, Contract) 15 | }) 16 | 17 | it("throws an error if contract is not known", () => { 18 | // assertThrow 19 | assert.throw(() => { 20 | qtum.contract("test/contracts/Unknown.sol") 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/TxReceiptPromise.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3" 2 | 3 | import { 4 | IRPCGetTransactionReceiptResult, 5 | IRPCGetTransactionRequest, 6 | IRPCGetTransactionResult, 7 | QtumRPC, 8 | } from "./QtumRPC" 9 | import { sleep } from "./sleep" 10 | 11 | export type TxReceiptConfirmationHandler = ( 12 | tx: IRPCGetTransactionResult, 13 | receipt: IRPCGetTransactionReceiptResult, 14 | ) => any 15 | 16 | const EVENT_CONFIRM = "confirm" 17 | 18 | // tslint:disable-next-line:no-empty-interface 19 | export interface ITxReceiptConfirmOptions { 20 | pollInterval?: number 21 | } 22 | 23 | export class TxReceiptPromise { 24 | private _emitter: EventEmitter 25 | 26 | constructor(private _rpc: QtumRPC, public txid: string) { 27 | this._emitter = new EventEmitter() 28 | } 29 | 30 | // TODO should return parsed logs with the receipt 31 | public async confirm( 32 | confirm: number = 6, 33 | opts: ITxReceiptConfirmOptions = {}, 34 | ): Promise { 35 | const minconf = confirm 36 | const pollInterval = opts.pollInterval || 3000 37 | 38 | const hasTxWaitSupport = await this._rpc.checkTransactionWaitSupport() 39 | 40 | // if hasTxWaitSupport, make one long-poll per confirmation 41 | let curConfirmation = 1 42 | // if !hasTxWaitSupport, poll every interval until tx.confirmations increased 43 | let lastConfirmation = 0 44 | 45 | while (true) { 46 | const req: IRPCGetTransactionRequest = { txid: this.txid } 47 | 48 | if (hasTxWaitSupport) { 49 | req.waitconf = curConfirmation 50 | } 51 | 52 | const tx = await this._rpc.getTransaction(req) 53 | 54 | if (tx.confirmations > 0) { 55 | const receipt = await this._rpc.getTransactionReceipt({ txid: tx.txid }) 56 | 57 | if (!receipt) { 58 | throw new Error("Cannot get transaction receipt") 59 | } 60 | 61 | // TODO augment receipt2 with parsed logs 62 | const receipt2 = receipt 63 | 64 | // const ctx = new ConfirmedTransaction(this.contract.info.abi, tx, receipt) 65 | 66 | if (tx.confirmations > lastConfirmation) { 67 | // confirmation increased since last check 68 | curConfirmation = tx.confirmations 69 | this._emitter.emit(EVENT_CONFIRM, tx, receipt2) 70 | // TODO emit update event 71 | // txUpdated(ctx) 72 | } 73 | 74 | if (tx.confirmations >= minconf) { 75 | // reached number of required confirmations. done 76 | return receipt2 77 | } 78 | } 79 | 80 | lastConfirmation = tx.confirmations 81 | 82 | if (hasTxWaitSupport) { 83 | // long-poll for one additional confirmation 84 | curConfirmation++ 85 | } else { 86 | await sleep(pollInterval + Math.random() * 200) 87 | } 88 | } 89 | } 90 | 91 | public onConfirm(fn: TxReceiptConfirmationHandler) { 92 | this._emitter.on(EVENT_CONFIRM, fn) 93 | } 94 | 95 | public offConfirm(fn: TxReceiptConfirmationHandler) { 96 | this._emitter.off(EVENT_CONFIRM, fn) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/abi.ts: -------------------------------------------------------------------------------- 1 | import { IABIMethod, IETHABI, ILogItem, LogDecoder } from "./ethjs-abi" 2 | 3 | const { 4 | decodeParams, 5 | encodeMethod, 6 | logDecoder, 7 | configure: configureABI, 8 | } = require("qtumjs-ethjs-abi") as IETHABI 9 | 10 | configureABI({ noHexStringPrefix: true }) 11 | 12 | import { ITransactionLog } from "./QtumRPC" 13 | 14 | export function encodeInputs(method: IABIMethod, args: any[] = []): string { 15 | const calldata = encodeMethod(method, args) 16 | return calldata 17 | } 18 | 19 | export function decodeOutputs(method: IABIMethod, outputData: string): any[] { 20 | const types = method.outputs.map((output) => output.type) 21 | 22 | // FIXME: would be nice to explicitly request for Array result 23 | const result = decodeParams(types, outputData) 24 | 25 | // Convert result to normal array... 26 | const values = [] 27 | for (let i = 0; i < types.length; i++) { 28 | values[i] = result[i] 29 | } 30 | 31 | return values 32 | } 33 | 34 | /** 35 | * A decoded Solidity event log 36 | */ 37 | export interface IDecodedSolidityEvent { 38 | /** 39 | * The event's name 40 | */ 41 | type: string 42 | 43 | /** 44 | * Event parameters as a key-value map 45 | */ 46 | [key: string]: any 47 | } 48 | 49 | export class ContractLogDecoder { 50 | private _decoder: LogDecoder 51 | 52 | constructor(public abi: IABIMethod[]) { 53 | this._decoder = logDecoder(abi) 54 | } 55 | 56 | public decode(rawlog: ILogItem): IDecodedSolidityEvent | null { 57 | const result = this._decoder([rawlog]) 58 | 59 | if (result.length === 0) { 60 | return null 61 | } 62 | 63 | const log = result[0] 64 | 65 | return log as any 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ethjs-abi.ts: -------------------------------------------------------------------------------- 1 | // partial type definitions for ethjs-abi 2 | export interface IABIMethod { 3 | name: string 4 | type: string 5 | payable: boolean 6 | inputs: IABIInput[] 7 | outputs: IABIOutput[] 8 | constant: boolean 9 | anonymous: boolean 10 | } 11 | 12 | export interface IABIInput { 13 | name: string 14 | type: string 15 | indexed: boolean 16 | } 17 | 18 | export interface IABIOutput { 19 | name: string 20 | type: string 21 | indexed: boolean 22 | } 23 | 24 | export interface IResult { 25 | [key: string]: any 26 | } 27 | 28 | export interface ILogItem { 29 | data: string 30 | topics: string[] 31 | } 32 | 33 | export interface IParsedLog extends IResult { 34 | _eventName: string 35 | [key: string]: any 36 | } 37 | 38 | export type LogDecoder = (logs: ILogItem[]) => IParsedLog[] 39 | 40 | export interface IETHABI { 41 | encodeMethod(method: IABIMethod, values: any[]): string 42 | 43 | decodeParams( 44 | types: string[], 45 | data: string, 46 | useNumberedParams?: boolean, 47 | ): IResult 48 | decodeParams( 49 | names: string[], 50 | types: string[], 51 | data: string, 52 | useNumberedParams?: boolean, 53 | ): IResult 54 | 55 | decodeLogItem( 56 | eventObject: IABIMethod, 57 | log: ILogItem, 58 | useNumberedParams?: boolean, 59 | ): IResult 60 | 61 | logDecoder(abi: IABIMethod[], useNumberedParams?: boolean): LogDecoder 62 | 63 | configure(opts: any): any 64 | } 65 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Browser polyfill required by ethjs-abi 2 | // https://github.com/ethjs/ethjs-abi/blob/5e2d4c3b7207111c143ca30d01d743c28cfb52f6/src/utils/index.js#L28 3 | if (typeof Buffer === "undefined") { 4 | const { Buffer } = require("buffer") 5 | Object.assign(window, { 6 | Buffer, 7 | }) 8 | } 9 | 10 | export * from "./abi" 11 | export * from "./Contract" 12 | export * from "./QtumRPC" 13 | export * from "./Qtum" 14 | export * from "./TxReceiptPromise" 15 | export * from "./ethjs-abi" 16 | -------------------------------------------------------------------------------- /src/sleep.ts: -------------------------------------------------------------------------------- 1 | export async function sleep(ms: number) { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, ms) 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | 3 | import { QtumRPC } from "../QtumRPC" 4 | 5 | export const rpcURL = process.env.QTUM_RPC || "http://qtum:test@localhost:3889" 6 | 7 | export const rpc = new QtumRPC(rpcURL) 8 | 9 | export const repoData = require("../../solar.development.json") 10 | 11 | export async function generateBlock(n = 1) { 12 | // generate to a throwaway address 13 | return rpc.rawCall("generatetoaddress", [n, "qUdPrkrdbmWD5m21mKEr5euZpFDsQHWzsG"]) 14 | } 15 | 16 | export async function assertThrow( 17 | fn: () => Promise, 18 | msg?: string, 19 | report?: (err: any) => void, 20 | ) { 21 | let errorThrown: any = null 22 | 23 | try { 24 | await fn() 25 | } catch (err) { 26 | errorThrown = err 27 | } 28 | 29 | // assert.erro 30 | if (errorThrown && report) { 31 | report(errorThrown) 32 | } 33 | 34 | assert( 35 | errorThrown != null, 36 | msg ? `Expects error to be thrown: ${msg}` : "Expects error to be thrown", 37 | ) 38 | 39 | // assert.isNotNull(errorThrown, ) 40 | } 41 | -------------------------------------------------------------------------------- /test/contracts/ArrayArguments.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | // https://github.com/qtumproject/qtumjs/issues/4 4 | contract ArrayArguments { 5 | function takeArray(address[] memory addresses) public {} 6 | } 7 | -------------------------------------------------------------------------------- /test/contracts/LogOfDependantContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | import "./LogOfDependantContractChild.sol"; 4 | 5 | // https://github.com/qtumproject/qtumjs/issues/4 6 | contract LogOfDependantContract { 7 | LogOfDependantContractChild testContract; 8 | 9 | constructor() public { 10 | testContract = new LogOfDependantContractChild(); 11 | } 12 | 13 | function emitLog() public { 14 | testContract.emitFoo(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/contracts/LogOfDependantContractChild.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | // Related to: LogOfDependantContract.sol 4 | // https://github.com/qtumproject/qtumjs/issues/4 5 | 6 | contract LogOfDependantContractChild { 7 | event LogOfDependantContractChildEvent(string data); 8 | 9 | function emitFoo() public { 10 | emit LogOfDependantContractChildEvent("Foo!"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/contracts/Logs.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | contract Logs { 4 | 5 | event FooEvent(string a); 6 | event BarEvent(string a); 7 | event BazEvent(string a); 8 | 9 | function emitFooEvent(string memory a) public returns(string memory) { 10 | emit FooEvent(a); 11 | return a; 12 | } 13 | 14 | function emitMultipleEvents(string memory a) public returns(string memory) { 15 | emit FooEvent(a); 16 | emit BarEvent(a); 17 | emit BazEvent(a); 18 | return a; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/contracts/MethodOverloading.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | contract MethodOverloading { 4 | event Foo1(uint256 a); 5 | event Foo2(uint256 a, uint256 b); 6 | 7 | function foo() public returns(string memory) { 8 | return "foo()"; 9 | } 10 | 11 | function foo(uint256 _a) public returns(string memory) { 12 | return "foo(uint256)"; 13 | } 14 | 15 | function foo(string memory _a) public returns(string memory) { 16 | return "foo(string)"; 17 | } 18 | 19 | function foo(uint256 _a, uint256 _b) public returns(string memory) { 20 | return "foo(uint256,uint256)"; 21 | } 22 | 23 | function foo(int256 _a, int256 _b) public returns(string memory) { 24 | return "foo(int256,int256)"; 25 | } 26 | 27 | function foo(int256 _a, int256 _b, int256 _c) public returns(string memory) { 28 | return "foo(int256,int256,int256)"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/contracts/Methods.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | contract Methods { 4 | 5 | uint256 foo; 6 | 7 | function setFoo(uint256 _foo) public { 8 | foo = _foo; 9 | } 10 | 11 | function getFoo() public view returns(uint256) { 12 | return foo; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation: */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "lib", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 26 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 27 | 28 | /* Additional Checks */ 29 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 33 | 34 | /* Module Resolution Options */ 35 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 36 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 37 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 38 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 39 | // "typeRoots": [], /* List of folders to include type definitions from. */ 40 | "types": [ 41 | "node" 42 | ] /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | 45 | /* Source Map Options */ 46 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 47 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 48 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 49 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 50 | 51 | /* Experimental Options */ 52 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 53 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 54 | }, 55 | "include": [ 56 | "src/**.ts", 57 | "src/**.d.ts" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "no-var-requires": false, 9 | "semicolon": [ 10 | true, 11 | "never" 12 | ], 13 | "trailing-comma": [ 14 | true 15 | ], 16 | "object-literal-sort-keys": false, 17 | "variable-name": [ 18 | true, 19 | "allow-leading-underscore" 20 | ], 21 | "ordered-imports": false 22 | }, 23 | "rulesDirectory": [] 24 | } 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const mode = process.env.NODE_ENV || "production"; 4 | 5 | module.exports = { 6 | mode, 7 | entry: "./src/index.ts", 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.ts$/, 12 | use: "ts-loader", 13 | exclude: /node_modules/ 14 | } 15 | ] 16 | }, 17 | resolve: { 18 | extensions: [".ts", ".js"] 19 | }, 20 | output: { 21 | filename: "qtum.js", 22 | path: path.resolve(__dirname, "dist"), 23 | library: "Qtum", 24 | libraryTarget: "umd" 25 | } 26 | }; 27 | --------------------------------------------------------------------------------