├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── bin ├── balance ├── proxy ├── send └── unlock-and-send ├── blockchain ├── EVM │ ├── contracts │ │ ├── CryptoErc20.sol │ │ ├── ERC20.sol │ │ ├── IERC20.sol │ │ ├── Migrations.sol │ │ ├── SafeMath.sol │ │ └── SendToMany.sol │ ├── migrations │ │ ├── 1_initial_migration.js │ │ ├── 2_erc20.js │ │ └── 3_sendToMany.js │ ├── test │ │ └── sendToMany.js │ └── truffle.js └── solana │ └── test │ ├── keypair │ ├── id.json │ ├── id2.json │ ├── id3.json │ └── validator.json │ └── startSolana.sh ├── config.example.js ├── docker-compose.yml ├── index.js ├── lib ├── args.js ├── bch │ └── BchRpc.js ├── btc │ ├── BtcRpc.js │ └── bitcoin.js ├── doge │ └── DogeRpc.js ├── erc20 │ ├── Erc20Rpc.js │ └── erc20.json ├── eth │ ├── EthRpc.js │ └── chains.js ├── index.js ├── lnd │ └── LndRpc.js ├── ltc │ └── LtcRpc.js ├── matic │ └── MaticRpc.js ├── sol │ ├── SolRpc.js │ ├── SplRpc.js │ ├── error_messages.js │ └── transaction-parser.js └── xrp │ ├── XrpClientAdapter.js │ └── XrpRpc.js ├── package-lock.json ├── package.json ├── start.dockerfile └── tests ├── bch.js ├── btc.js ├── docker ├── Dockerfile-test ├── ganache.Dockerfile ├── geth-keystore │ ├── UTC--2022-09-02T14-12-02.445263618Z--00a329c0648769a73afac7f9381e08fb43dbea72 │ └── pw ├── rippled.Dockerfile ├── rippled.cfg ├── solana.Dockerfile └── solc-v0.4.24 │ └── solc ├── doge.js ├── erc20.js ├── eth.js ├── evm.js ├── lnd.js ├── ltc.js ├── sol.js ├── spl.js └── xrp.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 3 | version: 2 4 | jobs: 5 | build: 6 | machine: 7 | image: default # https://discuss.circleci.com/t/linux-image-deprecations-and-eol-for-2024/50177 8 | docker_layer_caching: false 9 | working_directory: ~/crypto-rpc 10 | steps: 11 | - checkout 12 | - run: 13 | name: Build service 14 | command: npm test 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # EditorConfig helps developers define and maintain consistent 3 | # coding styles between different editors and IDEs 4 | # http://editorconfig.org 5 | 6 | root = true 7 | 8 | [*] 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /blockchain 2 | /client/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | node: true, 6 | es2022: true, 7 | mocha: true 8 | }, 9 | extends: 'eslint:recommended', 10 | parserOptions: { 11 | ecmaVersion: 2022, 12 | sourceType: 'module' 13 | }, 14 | rules: { 15 | indent: ['error', 2, { SwitchCase: 1 }], 16 | 'linebreak-style': ['error', 'unix'], 17 | quotes: ['error', 'single'], 18 | semi: ['error', 'always' ], 19 | 'no-console': ['error', { allow: ['warn', 'error'] }], 20 | 'no-async-promise-executor': 'off', 21 | 'no-prototype-builtins': 'off', 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.csv 3 | *.dat 4 | *.iml 5 | *.log 6 | *.out 7 | *.pid 8 | *.seed 9 | *.sublime-* 10 | *.swo 11 | *.swp 12 | *.tgz 13 | *.xml 14 | .DS_Store 15 | .idea 16 | .project 17 | .strong-pm 18 | .vscode 19 | coverage 20 | node_modules 21 | npm-debug.log 22 | dist 23 | src/vendor 24 | src/dist 25 | config.js 26 | blockchain/EVM/build 27 | blockchain/solana/test/data 28 | tmp/ 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | ``` 4 | // get balance of all accounts 5 | ./bin/balance --node ETHNode --currency ETH 6 | 7 | // get balance of USDC for specific account 8 | ./bin/balance --node ETHNode --currency USDC --address 0xac2c37f15B77Ac5aC56fb8643cb02cb18F82C246 9 | ./bin/send --node ETHNode --currency USDC --amount 1111111 --address 0xac2c37f15B77Ac5aC56fb8643cb02cb18F82C246 10 | 11 | // Get balance of bitcoin wallet 12 | ./bin/balance --node BTCNode --currency BTC 13 | ``` 14 | -------------------------------------------------------------------------------- /bin/balance: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * BalanceProgram.start 4 | * 5 | * @param config 6 | * ETHNode: { 7 | * chain: 'ETH', 8 | * host: 'localhost', 9 | * rpcPort: '8545', 10 | * protocol: 'http', 11 | * tokens : { 12 | * GUSD: { 13 | * tokenContractAddress: '0xd0683a2f4e9ecc9ac6bb41090b4269f7cacdd5d4', 14 | * type: 'ERC20' 15 | * }, 16 | * USDC: { 17 | * tokenContractAddress: '0xc92e381c387edbfd2e2112f3054896dd20ac3d31', 18 | * type: 'ERC20' 19 | * } 20 | * } 21 | *} 22 | */ 23 | const BalanceProgram = { 24 | start: (program) => { 25 | const CryptoRPC = require('../lib'); 26 | const { 27 | currency, 28 | address, 29 | host, 30 | port, 31 | user, 32 | password, 33 | chain, 34 | protocol, 35 | rpcUser, 36 | rpcPass, 37 | rpcPort, 38 | token, 39 | cert, 40 | macaroon 41 | } = program; 42 | 43 | let rpcs = new CryptoRPC({ 44 | chain, 45 | host, 46 | rpcPort: port || rpcPort, 47 | rpcUser: user || rpcUser, 48 | rpcPass: password || rpcPass, 49 | protocol, 50 | token, 51 | cert, 52 | macaroon, 53 | }); 54 | return rpcs.getBalance({currency, address}); 55 | } 56 | }; 57 | 58 | if (require.main === module) { 59 | const params = require('../lib/args'); 60 | BalanceProgram.start(params) 61 | .then(balance => process.stdout.write(`${balance}\n`)) 62 | .catch(err => console.error(err)); 63 | } 64 | 65 | 66 | module.exports = BalanceProgram; 67 | -------------------------------------------------------------------------------- /bin/proxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * ProxyProgram.start 4 | */ 5 | const ProxyProgram = { 6 | start: (program) => { 7 | const CryptoRPC = require('../lib'); 8 | const { 9 | currency, 10 | host, 11 | port, 12 | user, 13 | password, 14 | chain, 15 | protocol, 16 | rpcUser, 17 | rpcPass, 18 | rpcPort, 19 | token, 20 | params, 21 | method, 22 | } = program; 23 | 24 | let rpcs = new CryptoRPC({ 25 | chain, 26 | host, 27 | rpcPort: port || rpcPort, 28 | rpcUser: user || rpcUser, 29 | rpcPass: password || rpcPass, 30 | protocol, 31 | token, 32 | }); 33 | const proxy = rpcs.get(currency); 34 | return proxy.asyncCall(method, [...params.split(/,/)]); 35 | } 36 | }; 37 | 38 | if (require.main === module) { 39 | const params = require('../lib/args'); 40 | ProxyProgram.start(params) 41 | .then(result => process.stdout.write(`${result}\n`)) 42 | .catch(err => console.error(err)); 43 | } 44 | 45 | 46 | module.exports = ProxyProgram; 47 | -------------------------------------------------------------------------------- /bin/send: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * SendProgram 4 | * 5 | * @param config 6 | * ETHNode: { 7 | * chain: 'ETH', 8 | * host: 'localhost', 9 | * rpcPort: '8545', 10 | * protocol: 'http', 11 | * tokens : { 12 | * GUSD: { 13 | * tokenContractAddress: '0xd0683a2f4e9ecc9ac6bb41090b4269f7cacdd5d4', 14 | * type: 'ERC20' 15 | * }, 16 | * USDC: { 17 | * tokenContractAddress: '0xc92e381c387edbfd2e2112f3054896dd20ac3d31', 18 | * type: 'ERC20' 19 | * } 20 | * } 21 | *} 22 | */ 23 | const SendProgram = { 24 | start: (program) => { 25 | const CryptoRPC = require('../lib'); 26 | const { 27 | currency, 28 | address, 29 | host, 30 | port, 31 | user, 32 | password, 33 | chain, 34 | protocol, 35 | rpcUser, 36 | rpcPass, 37 | rpcPort, 38 | token, 39 | amount, 40 | unlock, 41 | } = program; 42 | 43 | let rpcs = new CryptoRPC({ 44 | chain, 45 | host, 46 | rpcPort: port || rpcPort, 47 | rpcUser: user || rpcUser, 48 | rpcPass: password || rpcPass, 49 | protocol, 50 | token, 51 | }); 52 | 53 | const sendParams = { currency, address, amount }; 54 | if (unlock) { 55 | return rpcs.unlockAndSendToAddress(sendParams); 56 | } 57 | return rpcs.sendToAddress(sendParams); 58 | } 59 | }; 60 | 61 | if (require.main === module) { 62 | const params = require('../lib/args'); 63 | SendProgram.start(params) 64 | .then(balance => process.stdout.write(`${balance}\n`)) 65 | .catch(err => console.error(err)); 66 | } 67 | 68 | module.exports = SendProgram; 69 | -------------------------------------------------------------------------------- /bin/unlock-and-send: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * UnlockSendProgram 4 | * 5 | * @param config 6 | * ETHNode: { 7 | * host: 'localhost', 8 | * rpcPort: '8545', 9 | * protocol: 'http', 10 | * currencies : { 11 | * GUSD: { 12 | * tokenContractAddress: '0xd0683a2f4e9ecc9ac6bb41090b4269f7cacdd5d4' 13 | * }, 14 | * USDC: { 15 | * tokenContractAddress: '0xc92e381c387edbfd2e2112f3054896dd20ac3d31' 16 | * } 17 | * } 18 | *} 19 | */ 20 | const UnlockSendProgram = { 21 | start: (config, program) => { 22 | const CryptoRPC = require('../lib'); 23 | 24 | async function main() { 25 | const { node, currency, address, amount } = program; 26 | const rpcHost = config[node]; 27 | if(rpcHost) { 28 | const { host, rpcPort, protocol, user, pass } = rpcHost; 29 | const currencyConfig = rpcHost.currencies[currency] || {}; 30 | let rpcs = new CryptoRPC({ 31 | host, 32 | rpcPort, 33 | user, 34 | pass, 35 | protocol, 36 | }, currencyConfig); 37 | 38 | rpcs.cmdlineUnlock(currency, 6000, (err, relock) => { 39 | rpcs.sendToAddress(currency, address, amount, (err, tx) => { 40 | if(err) console.error(err); 41 | global.console.log(tx); 42 | relock(); 43 | }); 44 | }); 45 | } else { 46 | console.error('ERROR: Node is not in the config'); 47 | } 48 | } 49 | main(); 50 | } 51 | }; 52 | 53 | 54 | if (require.main === module) { 55 | const config = require('../config'); 56 | const program = require('commander'); 57 | try{ 58 | program 59 | .option('--node ') 60 | .option('--currency ') 61 | .option('--address
') 62 | .option('--amount '); 63 | 64 | program.parse(process.argv); 65 | UnlockSendProgram.start(config, program); 66 | } catch (e) { 67 | global.console.log(e.message); 68 | program.help(); 69 | } 70 | } 71 | 72 | 73 | module.exports = UnlockSendProgram; 74 | -------------------------------------------------------------------------------- /blockchain/EVM/contracts/CryptoErc20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | import "./ERC20.sol"; 3 | 4 | contract CryptoErc20 is ERC20 { 5 | 6 | string public name = "CryptoErc20 "; 7 | string public symbol = "CE20"; 8 | uint public decimals = 18; 9 | uint public INITIAL_SUPPLY = 1000 * 1000 * 1000 * 10 ** 18; 10 | 11 | constructor() public ERC20() { 12 | _totalSupply = INITIAL_SUPPLY; 13 | _balances[msg.sender] = INITIAL_SUPPLY; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /blockchain/EVM/contracts/ERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "./IERC20.sol"; 4 | import "./SafeMath.sol"; 5 | 6 | /** 7 | * @title Standard ERC20 token 8 | * 9 | * @dev Implementation of the basic standard token. 10 | * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md 11 | * Originally based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol 12 | */ 13 | contract ERC20 is IERC20 { 14 | using SafeMath for uint256; 15 | 16 | mapping (address => uint256) public _balances; 17 | 18 | mapping (address => mapping (address => uint256)) public _allowed; 19 | 20 | uint256 public _totalSupply; 21 | 22 | /** 23 | * @dev Total number of tokens in existence 24 | */ 25 | function totalSupply() public view returns (uint256) { 26 | return _totalSupply; 27 | } 28 | 29 | /** 30 | * @dev Gets the balance of the specified address. 31 | * @param owner The address to query the balance of. 32 | * @return An uint256 representing the amount owned by the passed address. 33 | */ 34 | function balanceOf(address owner) public view returns (uint256) { 35 | return _balances[owner]; 36 | } 37 | 38 | /** 39 | * @dev Function to check the amount of tokens that an owner allowed to a spender. 40 | * @param owner address The address which owns the funds. 41 | * @param spender address The address which will spend the funds. 42 | * @return A uint256 specifying the amount of tokens still available for the spender. 43 | */ 44 | function allowance( 45 | address owner, 46 | address spender 47 | ) 48 | public 49 | view 50 | returns (uint256) 51 | { 52 | return _allowed[owner][spender]; 53 | } 54 | 55 | /** 56 | * @dev Transfer token for a specified address 57 | * @param to The address to transfer to. 58 | * @param value The amount to be transferred. 59 | */ 60 | function transfer(address to, uint256 value) public returns (bool) { 61 | require(value <= _balances[msg.sender]); 62 | require(to != address(0)); 63 | 64 | _balances[msg.sender] = _balances[msg.sender].sub(value); 65 | _balances[to] = _balances[to].add(value); 66 | emit Transfer(msg.sender, to, value); 67 | return true; 68 | } 69 | 70 | /** 71 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 72 | * Beware that changing an allowance with this method brings the risk that someone may use both the old 73 | * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this 74 | * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: 75 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 76 | * @param spender The address which will spend the funds. 77 | * @param value The amount of tokens to be spent. 78 | */ 79 | function approve(address spender, uint256 value) public returns (bool) { 80 | require(spender != address(0)); 81 | 82 | _allowed[msg.sender][spender] = value; 83 | emit Approval(msg.sender, spender, value); 84 | return true; 85 | } 86 | 87 | /** 88 | * @dev Transfer tokens from one address to another 89 | * @param from address The address which you want to send tokens from 90 | * @param to address The address which you want to transfer to 91 | * @param value uint256 the amount of tokens to be transferred 92 | */ 93 | function transferFrom( 94 | address from, 95 | address to, 96 | uint256 value 97 | ) 98 | public 99 | returns (bool) 100 | { 101 | require(value <= _balances[from]); 102 | require(value <= _allowed[from][msg.sender]); 103 | require(to != address(0)); 104 | 105 | _balances[from] = _balances[from].sub(value); 106 | _balances[to] = _balances[to].add(value); 107 | _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value); 108 | emit Transfer(from, to, value); 109 | return true; 110 | } 111 | 112 | /** 113 | * @dev Increase the amount of tokens that an owner allowed to a spender. 114 | * approve should be called when allowed_[_spender] == 0. To increment 115 | * allowed value is better to use this function to avoid 2 calls (and wait until 116 | * the first transaction is mined) 117 | * From MonolithDAO Token.sol 118 | * @param spender The address which will spend the funds. 119 | * @param addedValue The amount of tokens to increase the allowance by. 120 | */ 121 | function increaseAllowance( 122 | address spender, 123 | uint256 addedValue 124 | ) 125 | public 126 | returns (bool) 127 | { 128 | require(spender != address(0)); 129 | 130 | _allowed[msg.sender][spender] = ( 131 | _allowed[msg.sender][spender].add(addedValue)); 132 | emit Approval(msg.sender, spender, _allowed[msg.sender][spender]); 133 | return true; 134 | } 135 | 136 | /** 137 | * @dev Decrease the amount of tokens that an owner allowed to a spender. 138 | * approve should be called when allowed_[_spender] == 0. To decrement 139 | * allowed value is better to use this function to avoid 2 calls (and wait until 140 | * the first transaction is mined) 141 | * From MonolithDAO Token.sol 142 | * @param spender The address which will spend the funds. 143 | * @param subtractedValue The amount of tokens to decrease the allowance by. 144 | */ 145 | function decreaseAllowance( 146 | address spender, 147 | uint256 subtractedValue 148 | ) 149 | public 150 | returns (bool) 151 | { 152 | require(spender != address(0)); 153 | 154 | _allowed[msg.sender][spender] = ( 155 | _allowed[msg.sender][spender].sub(subtractedValue) 156 | ); 157 | emit Approval(msg.sender, spender, _allowed[msg.sender][spender]); 158 | return true; 159 | } 160 | 161 | /** 162 | * @dev Internal function that mints an amount of the token and assigns it to 163 | * an account. This encapsulates the modification of balances such that the 164 | * proper events are emitted. 165 | * @param account The account that will receive the created tokens. 166 | * @param amount The amount that will be created. 167 | */ 168 | function _mint(address account, uint256 amount) internal { 169 | require(account != 0); 170 | _totalSupply = _totalSupply.add(amount); 171 | _balances[account] = _balances[account].add(amount); 172 | emit Transfer(address(0), account, amount); 173 | } 174 | 175 | /** 176 | * @dev Internal function that burns an amount of the token of a given 177 | * account. 178 | * @param account The account whose tokens will be burnt. 179 | * @param amount The amount that will be burnt. 180 | */ 181 | function _burn(address account, uint256 amount) internal { 182 | require(account != 0); 183 | require(amount <= _balances[account]); 184 | 185 | _totalSupply = _totalSupply.sub(amount); 186 | _balances[account] = _balances[account].sub(amount); 187 | emit Transfer(account, address(0), amount); 188 | } 189 | 190 | /** 191 | * @dev Internal function that burns an amount of the token of a given 192 | * account, deducting from the sender's allowance for said account. Uses the 193 | * internal burn function. 194 | * @param account The account whose tokens will be burnt. 195 | * @param amount The amount that will be burnt. 196 | */ 197 | function _burnFrom(address account, uint256 amount) internal { 198 | require(amount <= _allowed[account][msg.sender]); 199 | 200 | // Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted, 201 | // this function needs to emit an event with the updated approval. 202 | _allowed[account][msg.sender] = _allowed[account][msg.sender].sub(amount); 203 | _burn(account, amount); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /blockchain/EVM/contracts/IERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | /** 4 | * @title ERC20 interface 5 | * @dev see https://github.com/ethereum/EIPs/issues/20 6 | */ 7 | interface IERC20 { 8 | function totalSupply() external view returns (uint256); 9 | 10 | function balanceOf(address who) external view returns (uint256); 11 | 12 | function allowance(address owner, address spender) 13 | external view returns (uint256); 14 | 15 | function transfer(address to, uint256 value) external returns (bool); 16 | 17 | function approve(address spender, uint256 value) 18 | external returns (bool); 19 | 20 | function transferFrom(address from, address to, uint256 value) 21 | external returns (bool); 22 | 23 | event Transfer( 24 | address indexed from, 25 | address indexed to, 26 | uint256 value 27 | ); 28 | 29 | event Approval( 30 | address indexed owner, 31 | address indexed spender, 32 | uint256 value 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /blockchain/EVM/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /blockchain/EVM/contracts/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | /** 4 | * @title SafeMath 5 | * @dev Math operations with safety checks that revert on error 6 | */ 7 | library SafeMath { 8 | 9 | /** 10 | * @dev Multiplies two numbers, reverts on overflow. 11 | */ 12 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 13 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 14 | // benefit is lost if 'b' is also tested. 15 | // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 16 | if (a == 0) { 17 | return 0; 18 | } 19 | 20 | uint256 c = a * b; 21 | require(c / a == b); 22 | 23 | return c; 24 | } 25 | 26 | /** 27 | * @dev Integer division of two numbers truncating the quotient, reverts on division by zero. 28 | */ 29 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 30 | require(b > 0); // Solidity only automatically asserts when dividing by 0 31 | uint256 c = a / b; 32 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 33 | 34 | return c; 35 | } 36 | 37 | /** 38 | * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend). 39 | */ 40 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 41 | require(b <= a); 42 | uint256 c = a - b; 43 | 44 | return c; 45 | } 46 | 47 | /** 48 | * @dev Adds two numbers, reverts on overflow. 49 | */ 50 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 51 | uint256 c = a + b; 52 | require(c >= a); 53 | 54 | return c; 55 | } 56 | 57 | /** 58 | * @dev Divides two numbers and returns the remainder (unsigned integer modulo), 59 | * reverts when dividing by zero. 60 | */ 61 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 62 | require(b != 0); 63 | return a % b; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /blockchain/EVM/contracts/SendToMany.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | import "./IERC20.sol"; 3 | 4 | contract SendToMany { 5 | address owner; 6 | 7 | constructor() { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier isOwner() { 12 | require(msg.sender == owner); 13 | _; 14 | } 15 | 16 | function sendMany(address[] addresses, uint[] amounts, address tokenContract) public payable isOwner { 17 | require(addresses.length == amounts.length); 18 | uint sum = 0; 19 | for(uint i = 0; i < amounts.length; i++) { 20 | sum += amounts[i]; 21 | } 22 | if(tokenContract != 0x0) { 23 | IERC20 token = IERC20(tokenContract); 24 | require(token.allowance(msg.sender, address(this)) >= sum, "This contract is not allowed enough funds for this batch"); 25 | for(i = 0; i < addresses.length; i++) { 26 | require(token.transferFrom(msg.sender, addresses[i], amounts[i]), "token transfer failed"); 27 | } 28 | } else { 29 | require((address(this).balance + msg.value) >= sum, "ETH balance too low for this batch"); 30 | for(i = 0; i < addresses.length; i++) { 31 | addresses[i].transfer(amounts[i]); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /blockchain/EVM/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | var Migrations = artifacts.require('./Migrations.sol'); 3 | 4 | module.exports = function(deployer) { 5 | deployer.deploy(Migrations); 6 | }; 7 | -------------------------------------------------------------------------------- /blockchain/EVM/migrations/2_erc20.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | var ERC20 = artifacts.require('./CryptoErc20.sol'); 3 | 4 | module.exports = function(deployer) { 5 | deployer.deploy(ERC20 ); 6 | }; 7 | -------------------------------------------------------------------------------- /blockchain/EVM/migrations/3_sendToMany.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | var SendToMany = artifacts.require('./SendToMany.sol'); 3 | 4 | module.exports = function(deployer) { 5 | deployer.deploy(SendToMany); 6 | }; 7 | -------------------------------------------------------------------------------- /blockchain/EVM/test/sendToMany.js: -------------------------------------------------------------------------------- 1 | const SendToMany = artifacts.require('SendToMany'); 2 | const CryptoErc20 = artifacts.require('CryptoErc20'); 3 | const ZERO_ADDR = '0x0000000000000000000000000000000000000000'; 4 | contract('SendToMany', (accounts) => { 5 | it('should exist', async() => { 6 | const batcher = await SendToMany.deployed(); 7 | assert(batcher); 8 | }); 9 | 10 | it('should send ether', async() => { 11 | const batcher = await SendToMany.deployed(); 12 | const receivers = accounts.slice(1); 13 | const amounts = new Array(receivers.length).fill(1e18.toString()); 14 | const balanceBefore = await web3.eth.getBalance(accounts[0]);; 15 | console.log('Token balance before', balanceBefore.toString()); 16 | const sum = (1e18*receivers.length).toString(); 17 | await batcher.sendMany(receivers, amounts, ZERO_ADDR, {value: sum}); 18 | const balanceAfter = await web3.eth.getBalance(accounts[0]);; 19 | console.log('ETH balance after', balanceAfter.toString()); 20 | for(const receiver of receivers) { 21 | const balance = await web3.eth.getBalance(receiver); 22 | console.log('ETH Balance', receiver, ':', balance.toString()); 23 | } 24 | }); 25 | 26 | it('should have token it can send', async() => { 27 | const token = await CryptoErc20.deployed(); 28 | assert(token); 29 | }); 30 | 31 | it('should send tokens', async() => { 32 | const batcher = await SendToMany.deployed(); 33 | const token = await CryptoErc20.deployed(); 34 | const receivers = accounts.slice(1); 35 | const amounts = new Array(receivers.length).fill(1e18.toString()); 36 | const sum = (1e18*receivers.length).toString(); 37 | const balanceBefore = await token.balanceOf(accounts[0]); 38 | console.log('Token balance before', balanceBefore.toString()); 39 | await token.approve(batcher.address, sum); 40 | await batcher.sendMany(receivers, amounts, token.address); 41 | const balanceAfter = await token.balanceOf(accounts[0]); 42 | console.log('Token balance after', balanceAfter.toString()); 43 | for(const receiver of receivers) { 44 | const balance = await token.balanceOf(receiver); 45 | console.log('Token Balance', receiver, ':', balance.toString()); 46 | } 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /blockchain/EVM/truffle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura API 13 | * keys are available for free at: infura.io/register 14 | * 15 | * > > Using Truffle V5 or later? Make sure you install the `web3-one` version. 16 | * 17 | * > > $ npm install truffle-hdwallet-provider@web3-one 18 | * 19 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 20 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 21 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 22 | * 23 | */ 24 | 25 | // const HDWallet = require('truffle-hdwallet-provider'); 26 | // const infuraKey = "fj4jll3k....."; 27 | // 28 | // const fs = require('fs'); 29 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 30 | 31 | module.exports = { 32 | /** 33 | * Networks define how you connect to your ethereum client and let you set the 34 | * defaults web3 uses to send transactions. If you don't specify one truffle 35 | * will spin up a development blockchain for you on port 9545 when you 36 | * run `develop` or `test`. You can ask a truffle command to use a specific 37 | * network from the command line, e.g 38 | * 39 | * $ truffle test --network 40 | */ 41 | 42 | networks: { 43 | // Useful for testing. The `development` name is special - truffle uses it by default 44 | // if it's defined here and no other network is specified at the command line. 45 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 46 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 47 | // options below to some value. 48 | // 49 | development_geth: { 50 | host: 'geth', // Localhost (default: none) 51 | port: 8545, // Standard Ethereum port (default: none) 52 | gas: 4700000, // Ropsten has a lower block limit than mainnet 53 | network_id: '*' // Any network (default: none) 54 | }, 55 | development_matic: { 56 | host: 'ganache', // Localhost (default: none) 57 | port: 8545, // Standard Ethereum port (default: none) 58 | gas: 4700000, // Ropsten has a lower block limit than mainnet 59 | network_id: '*' // Any network (default: none) 60 | }, 61 | }, 62 | 63 | 64 | // Set default mocha options here, use special reporters etc. 65 | mocha: { 66 | // timeout: 100000 67 | }, 68 | 69 | // Configure your compilers 70 | compilers: { 71 | solc: { 72 | version: 'native', // Fetch exact version from solc-bin (default: truffle's version) 73 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 74 | // settings: { // See the solidity docs for advice about optimization and evmVersion 75 | // optimizer: { 76 | // enabled: false, 77 | // runs: 200 78 | // }, 79 | // evmVersion: "byzantium" 80 | // } 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /blockchain/solana/test/keypair/id.json: -------------------------------------------------------------------------------- 1 | [116,69,205,66,216,181,214,32,36,171,241,160,150,232,188,235,103,123,252,48,214,24,3,3,62,30,86,207,200,244,51,112,125,28,121,144,126,16,219,102,36,155,53,86,197,85,53,212,14,162,165,146,107,170,151,139,23,187,186,201,157,175,125,199] -------------------------------------------------------------------------------- /blockchain/solana/test/keypair/id2.json: -------------------------------------------------------------------------------- 1 | [19,189,139,88,24,12,146,155,177,19,9,59,186,134,90,197,54,198,110,190,228,206,214,210,197,3,136,89,194,202,118,20,111,175,224,31,149,241,173,89,124,232,38,93,248,58,106,3,88,205,241,132,179,247,87,188,201,124,78,151,39,146,242,210] -------------------------------------------------------------------------------- /blockchain/solana/test/keypair/id3.json: -------------------------------------------------------------------------------- 1 | [111,197,242,14,118,44,66,203,60,194,40,110,92,232,238,66,8,252,188,88,11,83,103,105,48,172,125,30,146,126,150,226,209,157,201,11,117,149,72,146,24,180,136,1,49,113,203,216,165,60,57,180,135,207,103,204,255,56,145,144,182,123,224,245] -------------------------------------------------------------------------------- /blockchain/solana/test/keypair/validator.json: -------------------------------------------------------------------------------- 1 | [50,57,96,110,221,151,159,36,174,191,52,239,243,169,72,157,10,95,137,220,237,228,197,172,118,74,37,66,217,252,56,97,183,145,113,187,216,48,172,206,249,11,61,133,155,192,8,176,80,159,150,207,99,148,128,70,36,230,216,146,105,54,202,22] -------------------------------------------------------------------------------- /blockchain/solana/test/startSolana.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start the Solana test validator 4 | solana-test-validator --reset --ledger /solana/data > solana-validator.log & echo "Starting Solana" 5 | 6 | # Wait for the validator to start 7 | sleep 5 8 | 9 | # Extract the public key from the keypair file 10 | export PUBLIC_KEY=$(solana-keygen pubkey /solana/keypair/id.json) 11 | export PUBLIC_KEY2=$(solana-keygen pubkey /solana/keypair/id2.json) 12 | export PUBLIC_KEY3=$(solana-keygen pubkey /solana/keypair/id3.json) 13 | 14 | # Airdrop SOL to the provided keypair 15 | solana airdrop 100 $PUBLIC_KEY --url localhost 16 | echo "Public Key1: $PUBLIC_KEY" 17 | solana airdrop 100 $PUBLIC_KEY2 --url localhost 18 | echo "Public Key2: $PUBLIC_KEY2" 19 | echo "Public Key3: $PUBLIC_KEY3" 20 | 21 | # Tail the logs 22 | tail -f solana-validator.log -------------------------------------------------------------------------------- /config.example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | BTCNode: { 3 | chain: 'BTC', 4 | host: 'localhost', 5 | protocol: 'http', 6 | rpcPort: '20009', 7 | rpcUser: 'bitpaytest', 8 | rpcPass: 'local321', 9 | }, 10 | BCHNode: { 11 | chain: 'BCH', 12 | host: 'localhost', 13 | protocol: 'http', 14 | rpcPort: '20003', 15 | rpcUser: 'bitpaytest', 16 | rpcPass: 'local321' 17 | }, 18 | DogeNode: { 19 | chain: 'DOGE', 20 | host: 'localhost', 21 | protocol: 'http', 22 | rpcPort: '20004', 23 | rpcUser: 'bitpaytest', 24 | rpcPass: 'local321' 25 | }, 26 | XRPNode: { 27 | chain: 'XRP', 28 | host: 'localhost', 29 | protocol: 'ws', 30 | rpcPort: '6006', 31 | }, 32 | ETHNode: { 33 | chain: 'ETH', 34 | host: 'localhost', 35 | rpcPort: '8545', 36 | protocol: 'http', 37 | tokens : { 38 | GUSD: { 39 | tokenContractAddress: '0x00C3f2662F4F56623712BaC28179E7aDf952c0F0', 40 | type: 'ERC20' 41 | }, 42 | USDC: { 43 | tokenContractAddress: '0xc2258ea076cF2467960EE9B62264b9E17d59eFc9', 44 | type: 'ERC20' 45 | }, 46 | PAX: { 47 | tokenContractAddress: '0x531f6D8aFA88CC6966FD817340b2A5D7FA3750AD', 48 | type: 'ERC20' 49 | } 50 | } 51 | }, 52 | ARBNode: { 53 | chain: 'ARB', 54 | host: 'localhost', 55 | rpcPort: '8546', 56 | protocol: 'http', 57 | isEVM: true 58 | }, 59 | SOLNode: { 60 | chain: 'SOL', 61 | host: 'localhost', 62 | rpcPort: '8899', 63 | protocol: 'http' 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | start: 6 | build: 7 | context: . 8 | dockerfile: start.dockerfile 9 | volumes: 10 | - my_data1:/root/.lnd 11 | - my_data2:/root/.lnd2 12 | networks: 13 | default: 14 | ipv4_address: 172.28.0.21 15 | depends_on: 16 | - bitcoin 17 | - bitcoin-cash 18 | - dogecoin 19 | - rippled 20 | - litecoin 21 | - lightning 22 | - lightning2 23 | - geth 24 | - ganache 25 | - solana 26 | 27 | test_runner: 28 | build: 29 | context: . 30 | dockerfile: ./tests/docker/Dockerfile-test 31 | volumes: 32 | - my_data1:/root/.lnd 33 | - my_data2:/root/.lnd2 34 | networks: 35 | default: 36 | ipv4_address: 172.28.0.2 37 | depends_on: 38 | - bitcoin 39 | - bitcoin-cash 40 | - dogecoin 41 | - rippled 42 | - litecoin 43 | - lightning 44 | - lightning2 45 | - geth 46 | - ganache 47 | - solana 48 | 49 | bitcoin: 50 | image: kajoseph/bitcoin-core:22.0 51 | ports: 52 | - "8333:8333" 53 | networks: 54 | default: 55 | ipv4_address: 172.28.0.3 56 | command: 57 | -printtoconsole 58 | -regtest=1 59 | -txindex=1 60 | -listen=1 61 | -server=1 62 | -dnsseed=0 63 | -upnp=0 64 | -port=8332 65 | -rpcport=8333 66 | -rpcallowip=172.0.0.0/8 67 | -rpcbind=0.0.0.0 68 | -rpcuser=cryptorpc 69 | -rpcpassword=local321 70 | -fallbackfee=0.0002 71 | -zmqpubrawblock=tcp://0.0.0.0:38332 72 | -zmqpubrawtx=tcp://0.0.0.0:38333 73 | restart: always 74 | 75 | bitcoin-cash: 76 | image: zquestz/bitcoin-abc:0.21.7 77 | ports: 78 | - "9333:9333" 79 | networks: 80 | default: 81 | ipv4_address: 172.28.0.4 82 | command: 83 | bitcoind 84 | -printtoconsole 85 | -regtest=1 86 | -txindex=1 87 | -listen=1 88 | -server=1 89 | -dnsseed=0 90 | -upnp=0 91 | -port=9332 92 | -rpcport=9333 93 | -rpcallowip="172.0.0.0/8" 94 | -rpcbind="bitcoin-cash" 95 | -deprecatedrpc=generate 96 | -discover=0 97 | -rpcuser=cryptorpc 98 | -rpcpassword=local321 99 | restart: always 100 | 101 | dogecoin: 102 | image: casperstack/dogecoin 103 | ports: 104 | - "22555:22555" 105 | networks: 106 | default: 107 | ipv4_address: 172.28.0.6 108 | command: 109 | dogecoind 110 | -printtoconsole 111 | -regtest=1 112 | -txindex=1 113 | -listen=1 114 | -server=1 115 | -dnsseed=0 116 | -upnp=0 117 | -port=22555 118 | -rpcport=22555 119 | -rpcallowip="172.0.0.0/8" 120 | -rpcbind="dogecoin" 121 | -deprecatedrpc=generate 122 | -discover=0 123 | -rpcuser=cryptorpc 124 | -rpcpassword=local321 125 | restart: always 126 | 127 | geth: 128 | image: 0labs/geth:v1.10.21 129 | volumes: 130 | - ./tests/docker/geth-keystore:/keystore 131 | ports: 132 | - "9545:8545" 133 | networks: 134 | default: 135 | ipv4_address: 172.28.0.7 136 | command: 137 | geth 138 | --dev 139 | --datadir=/home/kjoseph/nodes/dev/geth 140 | --networkid=1337 141 | --http 142 | --http.api=web3,eth,debug,personal,net 143 | --http.corsdomain='*' 144 | --http.vhosts='*' 145 | --http.addr=0.0.0.0 146 | --http.port=8545 147 | --keystore=/keystore 148 | --allow-insecure-unlock 149 | --unlock=00a329c0648769a73afac7f9381e08fb43dbea72 150 | --password=/keystore/pw 151 | 152 | ganache: 153 | image: trufflesuite/ganache-cli:v6.12.2 154 | ports: 155 | - "10545:8545" 156 | networks: 157 | default: 158 | ipv4_address: 172.28.0.11 159 | command: 160 | -m "dose youth patient boring disagree tuna random tower tornado version violin around" 161 | -b 2 162 | -g 20000000000 163 | -p 8545 164 | -a 20 165 | 166 | rippled: 167 | networks: 168 | default: 169 | ipv4_address: 172.28.0.8 170 | build: 171 | context: . 172 | dockerfile: ./tests/docker/rippled.Dockerfile 173 | 174 | litecoin: 175 | image: uphold/litecoin-core:0.16.3 176 | ports: 177 | - "10333:10333" 178 | networks: 179 | default: 180 | ipv4_address: 172.28.0.9 181 | command: 182 | -printtoconsole 183 | -regtest=1 184 | -txindex=1 185 | -listen=1 186 | -server=1 187 | -irc=0 188 | -dnsseed=0 189 | -upnp=0 190 | -port=10332 191 | -rpcport=10333 192 | -rpcallowip=172.0.0.0/8 193 | -rpcbind="litecoin" 194 | -rpcuser=cryptorpc 195 | -rpcpassword=local321 196 | restart: always 197 | 198 | lightning: 199 | image: lightninglabs/lnd:v0.14.1-beta 200 | ports: 201 | - "11009:11009" 202 | volumes: 203 | - my_data1:/root/.lnd 204 | networks: 205 | default: 206 | ipv4_address: 172.28.0.5 207 | command: 208 | --tlsextraip=172.28.0.5 209 | --tlsextradomain=lightning 210 | --rpclisten=0.0.0.0:11009 211 | --bitcoin.active 212 | --bitcoin.regtest 213 | --bitcoin.node=bitcoind 214 | --bitcoind.rpchost=bitcoin:8333 215 | --bitcoind.rpcuser=cryptorpc 216 | --bitcoind.rpcpass=local321 217 | --bitcoind.zmqpubrawblock=tcp://bitcoin:38332 218 | --bitcoind.zmqpubrawtx=tcp://bitcoin:38333 219 | restart: always 220 | 221 | lightning2: 222 | image: sbhat96/lnd-v0.14.1-beta:latest 223 | ports: 224 | - "11010:11010" 225 | volumes: 226 | - my_data2:/root/.lnd2 227 | networks: 228 | default: 229 | ipv4_address: 172.28.0.10 230 | command: 231 | --lnddir=/root/.lnd2 232 | --tlsextraip=172.28.0.10 233 | --tlsextradomain=lightning 234 | --rpclisten=0.0.0.0:11010 235 | --bitcoin.active 236 | --bitcoin.regtest 237 | --bitcoin.node=bitcoind 238 | --bitcoind.rpchost=bitcoin:8333 239 | --bitcoind.rpcuser=cryptorpc 240 | --bitcoind.rpcpass=local321 241 | --bitcoind.zmqpubrawblock=tcp://bitcoin:38332 242 | --bitcoind.zmqpubrawtx=tcp://bitcoin:38333 243 | restart: always 244 | 245 | solana: 246 | image: solanalabs/solana:v1.18.26 247 | networks: 248 | default: 249 | ipv4_address: 172.28.0.12 250 | build: 251 | context: . 252 | dockerfile: ./tests/docker/solana.Dockerfile 253 | ports: 254 | - "8899:8899" 255 | - "8900:8900" 256 | environment: 257 | - RUST_LOG=solana=info 258 | 259 | volumes: 260 | my_data1: 261 | driver: local 262 | my_data2: 263 | driver: local 264 | 265 | networks: 266 | default: 267 | driver: bridge 268 | ipam: 269 | driver: default 270 | config: 271 | - subnet: 172.28.0.0/16 272 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | BalanceProgram: require('./bin/balance'), 3 | SendProgram: require('./bin/send'), 4 | CryptoRpc: require('./lib') 5 | }; 6 | -------------------------------------------------------------------------------- /lib/args.js: -------------------------------------------------------------------------------- 1 | const program = require('commander'); 2 | 3 | const ERC20Currencies = ['ETH', 'PAX', 'USDC', 'GUSD']; 4 | const chainEQCurrencies = ['BTC', 'BCH', 'XRP', 'DOGE', 'LTC', 'LNBTC']; 5 | 6 | program 7 | .option('--node ') 8 | .option('--currency ') 9 | .option('--address
') 10 | .option('--port ') 11 | .option('--host ') 12 | .option('--user ') 13 | .option('--password ') 14 | .option('--protocol ') 15 | .option('--amount ') 16 | .option('--token ') 17 | .option('--unlock ') 18 | .option('--cert ') 19 | .option('--macaroon ') 20 | // rpc method to call 21 | .option('--method ') 22 | // params should be comma seperated values 23 | .option('--params '); 24 | 25 | function chainFromCurrency(currency) { 26 | if (ERC20Currencies.includes(currency)) { 27 | return 'ETH'; 28 | } 29 | if (chainEQCurrencies.includes(currency)) { 30 | return currency; 31 | } 32 | throw new Error('Unknown Currency'); 33 | } 34 | 35 | let params; 36 | 37 | try{ 38 | program.parse(process.argv); 39 | } catch (e) { 40 | console.error(e.message); 41 | program.help(); 42 | process.exit(1); 43 | } 44 | 45 | try { 46 | const config = require('../config'); 47 | const rpcHost = config[program.node]; 48 | params = { 49 | ...rpcHost, 50 | ...program 51 | }; 52 | } catch (error) { 53 | params = { ...program }; 54 | } 55 | 56 | if (program.currency) { 57 | params.chain = chainFromCurrency(program.currency); 58 | } 59 | 60 | if (!params.protocol) { 61 | params.protocol = 'http'; 62 | } 63 | 64 | module.exports = params; 65 | -------------------------------------------------------------------------------- /lib/bch/BchRpc.js: -------------------------------------------------------------------------------- 1 | const BtcRpc = require('../btc/BtcRpc'); 2 | class BchRpc extends BtcRpc { 3 | async estimateFee() { 4 | const feeRate = await this.asyncCall('estimateFee', []); 5 | const satoshisPerKb = Math.round(feeRate * 1e8); 6 | const satoshisPerByte = satoshisPerKb / 1e3; 7 | return satoshisPerByte; 8 | } 9 | } 10 | module.exports = BchRpc; 11 | -------------------------------------------------------------------------------- /lib/btc/BtcRpc.js: -------------------------------------------------------------------------------- 1 | const BitcoinRPC = require('./bitcoin'); 2 | const promptly = require('promptly'); 3 | const util = require('util'); 4 | const EventEmitter = require('events'); 5 | 6 | const passwordPromptAsync = util.promisify(promptly.password); 7 | 8 | class BtcRpc { 9 | constructor(config) { 10 | this.config = config; 11 | const { 12 | rpcPort: port, 13 | rpcUser: user, 14 | rpcPass: pass, 15 | host, 16 | protocol 17 | } = config; 18 | this.rpc = new BitcoinRPC({ host, port, user, pass, protocol }); 19 | this.emitter = new EventEmitter(); 20 | } 21 | 22 | asyncCall(method, args) { 23 | return new Promise((resolve, reject) => { 24 | this.rpc[method](...args, (err, response) => { 25 | if (err instanceof Error) { 26 | return reject(err); 27 | } 28 | 29 | const { error, result } = response; 30 | if (error) { 31 | err = new Error(error.message); 32 | err.code = error.code; // used by methods below 33 | err.conclusive = true; // used by server 34 | return reject(err); 35 | } 36 | if (result && result.errors) { 37 | return reject(new Error(result.errors[0])); 38 | } 39 | return resolve(result); 40 | }); 41 | }); 42 | } 43 | 44 | async cmdlineUnlock({ time }) { 45 | return this.asyncCall('cmdlineUnlock', [time]); 46 | } 47 | 48 | async sendMany({ account, batch, options }) { 49 | let batchClone = Object.assign({}, batch); 50 | for (let tx in batch) { 51 | batchClone[tx] /= 1e8; 52 | } 53 | if (!account) { 54 | account = ''; 55 | } 56 | const paramArray = [account, batchClone]; 57 | if (options) { 58 | paramArray.push(options); 59 | } 60 | return this.asyncCall('sendMany', paramArray); 61 | } 62 | 63 | async sendToAddress({ address, amount }) { 64 | return this.asyncCall('sendToAddress', [address, amount / 1e8]); 65 | } 66 | 67 | async unlockAndSendToAddress({ address, amount, passphrase }) { 68 | if (passphrase === undefined) { 69 | passphrase = await passwordPromptAsync('> '); 70 | } 71 | await this.walletUnlock({ passphrase, time: 10800 }); 72 | const tx = await this.sendToAddress({ address, amount }); 73 | await this.walletLock(); 74 | return tx; 75 | } 76 | 77 | async unlockAndSendToAddressMany({ account, payToArray, passphrase, time = 10800, maxValue = 10*1e8, maxOutputs = 1 }) { 78 | let payToArrayClone = [...payToArray]; 79 | if (passphrase === undefined) { 80 | passphrase = await passwordPromptAsync('> '); 81 | } 82 | await this.walletUnlock({ passphrase, time }); 83 | let payToArrayResult = []; 84 | while (payToArrayClone.length) { 85 | let currentValue = 0; 86 | let currentOutputs = 0; 87 | let paymentsObj = {}; 88 | let paymentsArr = []; 89 | if (payToArrayClone.length < maxOutputs) { 90 | maxOutputs = payToArrayClone.length; 91 | } 92 | while (currentValue < maxValue && currentOutputs < maxOutputs) { 93 | const {address, amount, id} = payToArrayClone.shift(); 94 | paymentsArr.push({ address, amount, id }); 95 | const emitAttempt = { 96 | address, 97 | amount, 98 | id 99 | }; 100 | this.emitter.emit('attempt', emitAttempt); 101 | if (!paymentsObj[address]) { 102 | paymentsObj[address] = 0; 103 | } 104 | paymentsObj[address] += amount; 105 | currentValue += amount; 106 | currentOutputs++; 107 | } 108 | let emitData = { 109 | txid: '', 110 | vout: '', 111 | id: '', 112 | amount: '', 113 | address: '', 114 | }; 115 | let txid; 116 | let txDetails; 117 | try { 118 | txid = await this.sendMany({ account, batch:paymentsObj }); 119 | emitData.txid = txid; 120 | } catch (error) { 121 | emitData.error = error; 122 | } 123 | try { 124 | if (txid) { 125 | txDetails = await this.getTransaction({ txid }); 126 | } 127 | } catch (error) { 128 | console.error(`Unable to get transaction details for txid: ${txid}.`); 129 | console.error(error); 130 | } 131 | for (let payment of paymentsArr) { 132 | if (txDetails && txDetails.vout) { 133 | for (let vout of txDetails.vout) { 134 | if ( 135 | vout.scriptPubKey.address === payment.address || 136 | // Legacy 137 | (Array.isArray(vout.scriptPubKey.addresses) && vout.scriptPubKey.addresses[0].includes(payment.address)) 138 | ) { 139 | emitData.vout = vout.n; 140 | payment.vout = emitData.vout; 141 | } 142 | } 143 | } 144 | emitData.id = payment.id; 145 | emitData.amount = payment.amount; 146 | emitData.address = payment.address; 147 | payment.txid = emitData.txid; 148 | if (emitData.error) { 149 | this.emitter.emit('failure', emitData); 150 | payment.error = emitData.error; 151 | } else { 152 | this.emitter.emit('success', emitData); 153 | } 154 | payToArrayResult.push(payment); 155 | } 156 | } 157 | await this.walletLock(); 158 | this.emitter.emit('done'); 159 | return payToArrayResult; 160 | } 161 | 162 | async getWalletInfo() { 163 | return this.asyncCall('getWalletInfo', []); 164 | } 165 | 166 | async isWalletEncrypted() { 167 | const walletInfo = await this.getWalletInfo(); 168 | return walletInfo.hasOwnProperty('unlocked_until'); 169 | } 170 | 171 | async isWalletLocked() { 172 | const walletInfo = await this.getWalletInfo(); 173 | return walletInfo['unlocked_until'] === 0; 174 | } 175 | 176 | async walletUnlock({ passphrase, time }) { 177 | if (await this.isWalletEncrypted()){ 178 | await this.asyncCall('walletPassPhrase', [passphrase, time]); 179 | } 180 | this.emitter.emit('unlocked', time ); 181 | } 182 | 183 | async walletLock() { 184 | if (await this.isWalletEncrypted()){ 185 | await this.asyncCall('walletLock', []); 186 | } 187 | this.emitter.emit('locked'); 188 | } 189 | 190 | async estimateFee({ nBlocks, mode }) { 191 | const args = [nBlocks]; 192 | if (mode) { // We don't want args[1] to be undefined/null 193 | args.push(mode); 194 | } 195 | const { feerate: feeRate } = await this.asyncCall('estimateSmartFee', args); 196 | const satoshisPerKb = Math.round(feeRate * 1e8); 197 | const satoshisPerByte = satoshisPerKb / 1e3; 198 | return satoshisPerByte; 199 | } 200 | 201 | async getBalance() { 202 | const balanceInfo = await this.asyncCall('getWalletInfo', []); 203 | return balanceInfo.balance * 1e8; 204 | } 205 | 206 | async getBestBlockHash() { 207 | return this.asyncCall('getBestBlockHash', []); 208 | } 209 | 210 | async getTransaction({ txid, detail = false, verbosity }) { 211 | const tx = await this.getRawTransaction({ txid, verbosity }); 212 | 213 | if (tx && detail) { 214 | for (let input of tx.vin) { 215 | const prevTx = await this.getTransaction({ txid: input.txid }); 216 | const utxo = prevTx.vout[input.vout]; 217 | const { value } = utxo; 218 | const address = utxo.scriptPubKey.address || 219 | // Legacy 220 | (utxo.scriptPubKey.addresses && utxo.scriptPubKey.addresses.length && utxo.scriptPubKey.addresses[0]); 221 | input = Object.assign(input, { 222 | value, 223 | address, 224 | confirmations: prevTx.confirmations 225 | }); 226 | } 227 | tx.unconfirmedInputs = tx.vin.some(input => !input.confirmations || input.confirmations < 1); 228 | let totalInputValue = tx.vin.reduce( 229 | (total, input) => total + input.value * 1e8, 230 | 0 231 | ); 232 | let totalOutputValue = tx.vout.reduce( 233 | (total, output) => total + output.value * 1e8, 234 | 0 235 | ); 236 | tx.fee = totalInputValue - totalOutputValue; 237 | } 238 | 239 | return tx; 240 | } 241 | 242 | /** 243 | * Returns transactions for the node's wallet(s). Note, the RPC method is `listtransactions`, but actually it returns 244 | * a transaction object for each wallet address. For example, if you sent a tx with 2 inputs from your wallet, this 245 | * RPC method would return 2 tx objects with the same txid, one for each input. 246 | * @param {string} label defaults to '*', returns incoming transactions paying to addresses with the specified label 247 | * @param {number} count defaults to 10, the number of transactions to return 248 | * @param {number} skip defaults to 0, the number of transactions to skip 249 | * @param {boolean} inclWatchOnly defaults to true, include watch-only addresses in the returned transactions 250 | * @returns {Array} 251 | */ 252 | async getTransactions({ label = '*', count = 10, skip = 0, inclWatchOnly = true } = {}) { 253 | return this.asyncCall('listTransactions', [label, count, skip, inclWatchOnly]); 254 | } 255 | 256 | async getRawTransaction({ txid, verbosity = 1 }) { 257 | try { 258 | return await this.asyncCall('getRawTransaction', [txid, verbosity]); 259 | } catch (err) { 260 | if (err.code === -5) { 261 | return null; 262 | } 263 | throw err; 264 | } 265 | } 266 | 267 | async sendRawTransaction({ rawTx }) { 268 | return this.asyncCall('sendRawTransaction', [rawTx]); 269 | } 270 | 271 | async decodeRawTransaction({ rawTx }) { 272 | return this.asyncCall('decodeRawTransaction', [rawTx]); 273 | } 274 | 275 | async getBlock({ hash, verbose = 1 }) { 276 | return this.asyncCall('getBlock', [hash, verbose]); 277 | } 278 | 279 | async getBlockHash({ height }) { 280 | return this.asyncCall('getBlockHash', [height]); 281 | } 282 | 283 | async getConfirmations({ txid }) { 284 | const tx = await this.getTransaction({ txid }); 285 | if (!tx) { 286 | return null; 287 | } 288 | if (tx.blockhash === undefined) { 289 | return 0; 290 | } 291 | return tx.confirmations; 292 | } 293 | 294 | async getTip() { 295 | const blockchainInfo = await this.getServerInfo(); 296 | const { blocks: height, bestblockhash: hash } = blockchainInfo; 297 | return { height, hash }; 298 | } 299 | 300 | async getTxOutputInfo({ txid, vout, includeMempool = false, transformToBitcore }) { 301 | const txidInfo = await this.asyncCall('gettxout', [txid, vout, includeMempool]); 302 | if (!txidInfo) { 303 | this.emitter.emit('error', new Error(`No info found for ${txid}`)); 304 | return null; 305 | } 306 | if (transformToBitcore) { 307 | let bitcoreUtxo = { 308 | mintIndex: vout, 309 | mintTxid: txid, 310 | address: txidInfo.scriptPubKey.address || txidInfo.scriptPubKey.addresses[0], // Legacy 311 | script: txidInfo.scriptPubKey.hex, 312 | value: txidInfo.value, 313 | confirmations: txidInfo.confirmations 314 | }; 315 | return bitcoreUtxo; 316 | } 317 | return txidInfo; 318 | } 319 | 320 | async validateAddress({ address }) { 321 | const validateInfo = await this.asyncCall('validateaddress', [address]); 322 | const { isvalid } = validateInfo; 323 | return isvalid; 324 | } 325 | 326 | getAccountInfo() { 327 | return {}; 328 | } 329 | 330 | getServerInfo() { 331 | return this.asyncCall('getblockchaininfo', []); 332 | } 333 | } 334 | 335 | module.exports = BtcRpc; 336 | -------------------------------------------------------------------------------- /lib/btc/bitcoin.js: -------------------------------------------------------------------------------- 1 | var promptly = require('promptly'); 2 | var util = require('util'); 3 | var bitcoinDRPC = require('bitcoind-rpc'); 4 | 5 | function BitcoinRPC(opts) { 6 | opts = opts || {}; 7 | 8 | var protocol = opts.protocol; 9 | var args = { protocol: protocol }; // to allow nodes without ssl (protocol: 'http') 10 | bitcoinDRPC.call(this, args); 11 | 12 | this.host = opts.host; 13 | this.port = opts.port; 14 | this.user = opts.user; 15 | this.pass = opts.pass; 16 | this.httpOptions = { rejectUnauthorized: false }; 17 | } 18 | util.inherits(BitcoinRPC, bitcoinDRPC); 19 | 20 | BitcoinRPC.prototype.cmdlineUnlock = function(timeout, callback) { 21 | var self = this; 22 | self.getWalletInfo(function(err, result) { 23 | if (err) { 24 | console.error(err); 25 | return callback(err); 26 | } 27 | if ('unlocked_until' in result.result) { 28 | if (result['unlocked_until']) { 29 | throw new Error('wallet is currently unlocked'); 30 | } 31 | promptly.password('> ', function(err, phrase) { 32 | if (err) { 33 | return callback(err); 34 | } 35 | self.walletPassPhrase(phrase, timeout, function(err) { 36 | if (err) { 37 | return callback(err); 38 | } else { 39 | console.warn('wallet unlocked for ' + timeout + ' seconds'); 40 | return callback(null, function(doneLocking) { 41 | self.walletLock(function(err) { 42 | if (err) { 43 | console.error(err.message); 44 | } else { 45 | console.error('wallet locked'); 46 | } 47 | doneLocking && doneLocking(); 48 | }); 49 | }); 50 | } 51 | }); 52 | }); 53 | } else { 54 | process.nextTick(function() { 55 | callback(null, function(doneLocking) { 56 | if (doneLocking) { 57 | process.nextTick(doneLocking); 58 | } 59 | }); 60 | }); 61 | } 62 | }); 63 | }; 64 | 65 | module.exports = BitcoinRPC; 66 | -------------------------------------------------------------------------------- /lib/doge/DogeRpc.js: -------------------------------------------------------------------------------- 1 | const DogecoinRPC = require('dogecoind-rpc'); 2 | const BtcRpc = require('../btc/BtcRpc'); 3 | class DogeRpc extends BtcRpc { 4 | constructor(config) { 5 | super(config); 6 | const { 7 | rpcPort: port, 8 | rpcUser: user, 9 | rpcPass: pass, 10 | host, 11 | protocol 12 | } = config; 13 | this.rpc = new DogecoinRPC({ host, port, user, pass, protocol }); 14 | } 15 | 16 | async getBlock({ hash, verbose = true }) { 17 | return this.asyncCall('getBlock', [hash, verbose]); 18 | } 19 | } 20 | module.exports = DogeRpc; 21 | -------------------------------------------------------------------------------- /lib/erc20/Erc20Rpc.js: -------------------------------------------------------------------------------- 1 | const EthRPC = require('../eth/EthRpc'); 2 | const erc20 = require('./erc20.json'); 3 | const ethers = require('ethers'); 4 | 5 | class Erc20RPC extends EthRPC { 6 | constructor(config) { 7 | super(config); 8 | this.tokenContractAddress = config.tokenContractAddress; 9 | this.erc20Contract = new this.web3.eth.Contract( 10 | erc20, 11 | this.tokenContractAddress 12 | ); 13 | } 14 | 15 | // this will only work on ERC20 tokens with decimals 16 | async sendToAddress({ address, amount, fromAccount, passphrase, gasPrice, nonce, gas }) { 17 | if (!gasPrice) { 18 | gasPrice = await this.estimateGasPrice(); 19 | } 20 | const account = fromAccount || await this.getAccount(); 21 | const amountStr = Number(amount).toLocaleString('fullwide', { useGrouping: false }); 22 | const contractData = this.erc20Contract.methods 23 | .transfer(address, amountStr) 24 | .encodeABI(); 25 | 26 | if (passphrase) { 27 | this.emitter.emit('unlockedForOne'); 28 | } 29 | 30 | let result; 31 | try { 32 | result = await this.web3.eth.personal.sendTransaction( 33 | { 34 | from: account, 35 | gasPrice, 36 | data: contractData, 37 | to: this.tokenContractAddress, 38 | nonce, 39 | gas 40 | }, 41 | passphrase 42 | ); 43 | 44 | if (passphrase) { 45 | this.emitter.emit('locked'); 46 | } 47 | } catch (error) { 48 | this.emitter.emit('locked'); 49 | throw new Error(error); 50 | } 51 | 52 | return result; 53 | } 54 | 55 | async getBalance({ address }) { 56 | if (address) { 57 | const balance = await this.erc20Contract.methods 58 | .balanceOf(address) 59 | .call(); 60 | return balance; 61 | } else { 62 | const accounts = await this.web3.eth.getAccounts(); 63 | const balances = []; 64 | for (let account of accounts) { 65 | const balance = await this.getBalance({ address: account }); 66 | balances.push({ account, balance }); 67 | } 68 | return balances; 69 | } 70 | } 71 | 72 | async decodeRawTransaction({ rawTx }) { 73 | const decodedEthTx = await super.decodeRawTransaction({ rawTx }); 74 | if (decodedEthTx.data) { 75 | try { 76 | const erc20Interface = new ethers.utils.Interface(erc20); 77 | decodedEthTx.decodedData = await erc20Interface.parseTransaction({ data: decodedEthTx.data }); 78 | } catch (err) { 79 | decodedEthTx.decodedData = undefined; 80 | } 81 | } 82 | return decodedEthTx; 83 | } 84 | } 85 | 86 | module.exports = Erc20RPC; 87 | -------------------------------------------------------------------------------- /lib/erc20/erc20.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "constant": true, 3 | "inputs": [], 4 | "name": "name", 5 | "outputs": [{ 6 | "name": "", 7 | "type": "string" 8 | }], 9 | "payable": false, 10 | "stateMutability": "view", 11 | "type": "function" 12 | }, 13 | { 14 | "constant": false, 15 | "inputs": [{ 16 | "name": "_spender", 17 | "type": "address" 18 | }, 19 | { 20 | "name": "_value", 21 | "type": "uint256" 22 | } 23 | ], 24 | "name": "approve", 25 | "outputs": [{ 26 | "name": "", 27 | "type": "bool" 28 | }], 29 | "payable": false, 30 | "stateMutability": "nonpayable", 31 | "type": "function" 32 | }, 33 | { 34 | "constant": true, 35 | "inputs": [], 36 | "name": "totalSupply", 37 | "outputs": [{ 38 | "name": "", 39 | "type": "uint256" 40 | }], 41 | "payable": false, 42 | "stateMutability": "view", 43 | "type": "function" 44 | }, 45 | { 46 | "constant": false, 47 | "inputs": [{ 48 | "name": "_from", 49 | "type": "address" 50 | }, 51 | { 52 | "name": "_to", 53 | "type": "address" 54 | }, 55 | { 56 | "name": "_value", 57 | "type": "uint256" 58 | } 59 | ], 60 | "name": "transferFrom", 61 | "outputs": [{ 62 | "name": "", 63 | "type": "bool" 64 | }], 65 | "payable": false, 66 | "stateMutability": "nonpayable", 67 | "type": "function" 68 | }, 69 | { 70 | "constant": true, 71 | "inputs": [], 72 | "name": "decimals", 73 | "outputs": [{ 74 | "name": "", 75 | "type": "uint8" 76 | }], 77 | "payable": false, 78 | "stateMutability": "view", 79 | "type": "function" 80 | }, 81 | { 82 | "constant": true, 83 | "inputs": [{ 84 | "name": "_owner", 85 | "type": "address" 86 | }], 87 | "name": "balanceOf", 88 | "outputs": [{ 89 | "name": "balance", 90 | "type": "uint256" 91 | }], 92 | "payable": false, 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { 97 | "constant": true, 98 | "inputs": [], 99 | "name": "symbol", 100 | "outputs": [{ 101 | "name": "", 102 | "type": "string" 103 | }], 104 | "payable": false, 105 | "stateMutability": "view", 106 | "type": "function" 107 | }, 108 | { 109 | "constant": false, 110 | "inputs": [{ 111 | "name": "_to", 112 | "type": "address" 113 | }, 114 | { 115 | "name": "_value", 116 | "type": "uint256" 117 | } 118 | ], 119 | "name": "transfer", 120 | "outputs": [{ 121 | "name": "", 122 | "type": "bool" 123 | }], 124 | "payable": false, 125 | "stateMutability": "nonpayable", 126 | "type": "function" 127 | }, 128 | { 129 | "constant": true, 130 | "inputs": [{ 131 | "name": "_owner", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_spender", 136 | "type": "address" 137 | } 138 | ], 139 | "name": "allowance", 140 | "outputs": [{ 141 | "name": "", 142 | "type": "uint256" 143 | }], 144 | "payable": false, 145 | "stateMutability": "view", 146 | "type": "function" 147 | }, 148 | { 149 | "payable": true, 150 | "stateMutability": "payable", 151 | "type": "fallback" 152 | }, 153 | { 154 | "anonymous": false, 155 | "inputs": [{ 156 | "indexed": true, 157 | "name": "owner", 158 | "type": "address" 159 | }, 160 | { 161 | "indexed": true, 162 | "name": "spender", 163 | "type": "address" 164 | }, 165 | { 166 | "indexed": false, 167 | "name": "value", 168 | "type": "uint256" 169 | } 170 | ], 171 | "name": "Approval", 172 | "type": "event" 173 | }, 174 | { 175 | "anonymous": false, 176 | "inputs": [{ 177 | "indexed": true, 178 | "name": "from", 179 | "type": "address" 180 | }, 181 | { 182 | "indexed": true, 183 | "name": "to", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": false, 188 | "name": "value", 189 | "type": "uint256" 190 | } 191 | ], 192 | "name": "Transfer", 193 | "type": "event" 194 | } 195 | ] 196 | -------------------------------------------------------------------------------- /lib/eth/chains.js: -------------------------------------------------------------------------------- 1 | const chainConfig = { 2 | ETH: { 3 | priorityFee: 1 // in gwei 4 | }, 5 | MATIC: { 6 | priorityFee: 30 7 | }, 8 | OP: { 9 | priorityFee: 1 10 | }, 11 | ARB: { 12 | priorityFee: 0 // transactions are processed FIFO, no priorityFee fee is necessary for Arbitrum transactions 13 | }, 14 | BASE: { 15 | priorityFee: 1 16 | } 17 | }; 18 | 19 | module.exports = chainConfig; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const RpcClasses = { 2 | BTC: require('./btc/BtcRpc'), 3 | BCH: require('./bch/BchRpc'), 4 | ETH: require('./eth/EthRpc'), 5 | XRP: require('./xrp/XrpRpc'), 6 | DOGE: require('./doge/DogeRpc'), 7 | LTC: require('./ltc/LtcRpc'), 8 | LNBTC: require('./lnd/LndRpc'), 9 | MATIC: require('./matic/MaticRpc'), // keeping for backwards compatibility 10 | SOL: require('./sol/SolRpc'), 11 | }; 12 | 13 | const TokenClasses = { 14 | ETH: { 15 | native: require('./eth/EthRpc'), 16 | ERC20: require('./erc20/Erc20Rpc') 17 | }, 18 | MATIC: { 19 | native: require('./matic/MaticRpc'), 20 | ERC20: require('./erc20/Erc20Rpc') 21 | }, 22 | BTC: { 23 | native: require('./btc/BtcRpc') 24 | }, 25 | BCH: { 26 | native: require('./bch/BchRpc') 27 | }, 28 | XRP: { 29 | native: require('./xrp/XrpRpc') 30 | }, 31 | DOGE: { 32 | native: require('./doge/DogeRpc') 33 | }, 34 | LTC: { 35 | native: require('./ltc/LtcRpc') 36 | }, 37 | LNBTC: { 38 | native: require('./lnd/LndRpc') 39 | }, 40 | SOL: { 41 | native: require('./sol/SolRpc'), 42 | SPL: require('./sol/SplRpc') 43 | } 44 | }; 45 | 46 | class CryptoRpcProvider { 47 | 48 | /** 49 | * Constructor for CryptoRpcProvider class. 50 | * @param {Object} config - The configuration object. 51 | * @param {string} config.chain - The chain to connect to. 52 | * @param {boolean} config.isEVM - Optional flag indicating if the chain is EVM compatible. 53 | * @param {string} config.host - The host address for RPC connection. 54 | * @param {number} config.port - The port for RPC connection. 55 | * @param {string} config.rpcPort - The port for RPC connection (alternative). 56 | * @param {string} config.user - The username for RPC connection. 57 | * @param {string} config.rpcUser - The username for RPC connection (alternative). 58 | * @param {string} config.pass - The password for RPC connection. 59 | * @param {string} config.rpcPass - The password for RPC connection (alternative). 60 | * @param {string} config.protocol - The protocol for RPC connection. 61 | * @param {Object} config.tokens - Optional tokens configuration. 62 | */ 63 | constructor(config) { 64 | this.chain = config.chain; 65 | if (!RpcClasses[this.chain] && !config.isEVM) { 66 | throw new Error('Invalid chain specified'); 67 | } 68 | this.config = Object.assign({}, config, { 69 | host: config.host, 70 | port: config.port || config.rpcPort, 71 | user: config.user || config.rpcUser, 72 | pass: config.pass || config.rpcPass, 73 | protocol: config.protocol 74 | }); 75 | const rpcChain = !config.isEVM ? this.chain : 'ETH'; 76 | this.rpcs = { 77 | [this.chain]: new RpcClasses[rpcChain](this.config) 78 | }; 79 | if (config.tokens) { 80 | Object.entries(config.tokens).forEach(([token, tokenConfig]) => { 81 | const TokenClass = TokenClasses[rpcChain][tokenConfig.type]; 82 | const configForToken = Object.assign(tokenConfig, this.config); 83 | this.rpcs[token] = new TokenClass(configForToken); 84 | }); 85 | } 86 | } 87 | 88 | has(currency) { 89 | return !!this.rpcs[currency]; 90 | } 91 | 92 | get(currency = this.chain) { 93 | return this.rpcs[currency]; 94 | } 95 | 96 | cmdlineUnlock(params) { 97 | return this.get(params.currency).cmdlineUnlock(params); 98 | } 99 | 100 | getBalance(params) { 101 | return this.get(params.currency).getBalance(params); 102 | } 103 | 104 | sendToAddress(params) { 105 | return this.get(params.currency).sendToAddress(params); 106 | } 107 | 108 | walletLock(params) { 109 | return this.get(params.currency).walletLock(params); 110 | } 111 | 112 | unlockAndSendToAddress(params) { 113 | return this.get(params.currency).unlockAndSendToAddress(params); 114 | } 115 | 116 | unlockAndSendToAddressMany(params) { 117 | return this.get(params.currency).unlockAndSendToAddressMany(params); 118 | } 119 | 120 | estimateFee(params) { 121 | return this.get(params.currency).estimateFee(params); 122 | } 123 | 124 | estimateMaxPriorityFee(params) { 125 | const rpc = this.get(params.currency); 126 | return rpc.estimateMaxPriorityFee ? rpc.estimateMaxPriorityFee(params) : undefined; 127 | } 128 | 129 | getBestBlockHash(params) { 130 | return this.get(params.currency).getBestBlockHash(params); 131 | } 132 | 133 | getTransaction(params) { 134 | return this.get(params.currency).getTransaction(params); 135 | } 136 | 137 | getTransactions(params) { 138 | return this.get(params.currency).getTransactions(params); 139 | } 140 | 141 | getTransactionCount(params) { 142 | return this.get(params.currency).getTransactionCount(params); 143 | } 144 | 145 | getRawTransaction(params) { 146 | return this.get(params.currency).getRawTransaction(params); 147 | } 148 | 149 | sendRawTransaction(params) { 150 | return this.get(params.currency).sendRawTransaction(params); 151 | } 152 | 153 | decodeRawTransaction(params) { 154 | return this.get(params.currency).decodeRawTransaction(params); 155 | } 156 | 157 | getBlock(params) { 158 | return this.get(params.currency).getBlock(params); 159 | } 160 | 161 | getBlockHash(params) { 162 | return this.get(params.currency).getBlockHash(params); 163 | } 164 | 165 | getConfirmations(params) { 166 | return this.get(params.currency).getConfirmations(params); 167 | } 168 | 169 | getTip(params) { 170 | return this.get(params.currency).getTip(params); 171 | } 172 | 173 | getTxOutputInfo(params) { 174 | return this.get(params.currency).getTxOutputInfo(params); 175 | } 176 | 177 | validateAddress(params) { 178 | return this.get(params.currency).validateAddress(params); 179 | } 180 | 181 | getAccountInfo(params) { 182 | return this.get(params.currency).getAccountInfo(params); 183 | } 184 | 185 | getServerInfo(params) { 186 | return this.get(params.currency).getServerInfo(params); 187 | } 188 | } 189 | 190 | module.exports = CryptoRpcProvider; 191 | -------------------------------------------------------------------------------- /lib/lnd/LndRpc.js: -------------------------------------------------------------------------------- 1 | const Lightning = require('lightning'); 2 | const EventEmitter = require('events'); 3 | 4 | class LndRpc { 5 | constructor(config) { 6 | this.config = config; 7 | const { 8 | rpcPort: port, 9 | host, 10 | macaroon, 11 | cert, 12 | } = config; 13 | const socket = host + ':' + port; 14 | this.rpc = Lightning.authenticatedLndGrpc({ socket, macaroon, cert }); 15 | this.unauthenticatedRpc = Lightning.unauthenticatedLndGrpc({ socket, cert }); 16 | this.emitter = new EventEmitter(); 17 | } 18 | 19 | /** 20 | * Call any method in the lightning library 21 | * @param {String} method method in the lightning library to call 22 | * @param {Array} args array with an object that contains all of the params 23 | * @param {Boolean} unauthenticated (optional) use the unauthenticated rpc (default will use authenticated) 24 | * @returns {Promise} calls the lightning library method and resolves on success 25 | */ 26 | async asyncCall(method, args, unauthenticated) { 27 | let params = args ? args[0] : {}; 28 | params = unauthenticated ? { ...params, ...this.unauthenticatedRpc } : { ...params, ...this.rpc }; 29 | try { 30 | return await Lightning[method](params); 31 | } catch (err) { 32 | // some LND methods return event emitters directly, this checks for that and calls the lightning method again 33 | if (err.message && err.message.includes('TypeError')) { 34 | return Lightning[method](params); 35 | } else { 36 | err.conclusive = true; // used by server 37 | throw err; 38 | } 39 | } 40 | } 41 | 42 | async getWalletInfo() { 43 | return Lightning.getWalletInfo(this.rpc); 44 | } 45 | 46 | async getBalance() { 47 | const balanceInfo = await Lightning.getChainBalance(this.rpc); 48 | return balanceInfo.chain_balance; 49 | } 50 | 51 | async getTransaction({ txid }) { 52 | const getInvoiceObject = { id: txid, ...this.rpc }; 53 | return await Lightning.getInvoice(getInvoiceObject); 54 | } 55 | 56 | async createInvoice({ id, amount, expiry }) { 57 | const { channels } = await Lightning.getChannels(this.rpc); 58 | if (!channels.length) { 59 | throw new Error('No open channels to create invoice on'); 60 | } 61 | return await Lightning.createInvoice({ description: id, tokens: amount, expires_at: expiry, ...this.rpc }); 62 | } 63 | 64 | async walletCreate({ passphrase }) { 65 | const { seed } = await Lightning.createSeed(this.unauthenticatedRpc); 66 | await Lightning.createWallet({ seed, password: passphrase, ...this.unauthenticatedRpc }); 67 | } 68 | 69 | async walletUnlock({ passphrase }) { 70 | await Lightning.unlockWallet({ ...this.unauthenticatedRpc, password: passphrase }); 71 | } 72 | 73 | async getBTCAddress({ format='p2wpkh' }) { 74 | return await Lightning.createChainAddress({format, ...this.rpc}); 75 | } 76 | 77 | async createNewAuthenticatedRpc({ cert, macaroon, rpcPort, host }) { 78 | const socket = host + ':' + rpcPort; 79 | this.rpc = Lightning.authenticatedLndGrpc({ socket, macaroon, cert }); 80 | } 81 | 82 | async openChannel({ amount, pubkey, socket }) { 83 | return await Lightning.openChannel({ 84 | ...this.rpc, 85 | local_tokens: amount, 86 | partner_public_key: pubkey, 87 | partner_socket: socket 88 | }); 89 | } 90 | 91 | async subscribeToInvoices() { 92 | return Lightning.subscribeToInvoices(this.rpc); 93 | } 94 | 95 | async getTip() { 96 | return await Lightning.getNetworkInfo(this.rpc); 97 | } 98 | 99 | async getBestBlockHash() { 100 | const height = await Lightning.getHeight(this.rpc); 101 | return height.current_block_height; 102 | } 103 | 104 | async estimateFee() { 105 | return await Lightning.getFeeRates(this.rpc); 106 | } 107 | 108 | async getAccountInfo({ address }) { 109 | try { 110 | return await Lightning.getNode({ ...this.rpc, public_key: address }); 111 | } catch (err) { 112 | if (Array.isArray(err) && err[1] === 'NodeIsUnknown') { 113 | return null; 114 | } 115 | throw err; 116 | } 117 | } 118 | 119 | getServerInfo() { 120 | return this.getWalletInfo(); 121 | } 122 | } 123 | 124 | module.exports = LndRpc; 125 | -------------------------------------------------------------------------------- /lib/ltc/LtcRpc.js: -------------------------------------------------------------------------------- 1 | const LitecoinRPC = require('bitcoind-rpc'); 2 | const BtcRpc = require('../btc/BtcRpc'); 3 | class LtcRpc extends BtcRpc { 4 | constructor(config) { 5 | super(config); 6 | const { 7 | rpcPort: port, 8 | rpcUser: user, 9 | rpcPass: pass, 10 | host, 11 | protocol 12 | } = config; 13 | this.rpc = new LitecoinRPC({ host, port, user, pass, protocol }); 14 | } 15 | } 16 | module.exports = LtcRpc; 17 | -------------------------------------------------------------------------------- /lib/matic/MaticRpc.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const EthRpc = require('../eth/EthRpc'); 3 | 4 | class MaticRpc extends EthRpc { 5 | super(config) { 6 | this.config = config; 7 | this.web3 = this.getWeb3(this.config); 8 | this.account = this.config.account; 9 | this.emitter = new EventEmitter(); 10 | } 11 | } 12 | module.exports = MaticRpc; 13 | -------------------------------------------------------------------------------- /lib/sol/SplRpc.js: -------------------------------------------------------------------------------- 1 | const SolRPC = require('./SolRpc'); 2 | const SolKit = require('@solana/kit'); 3 | const SolToken = require('@solana-program/token'); 4 | const SOL_ERROR_MESSAGES = require('./error_messages'); 5 | 6 | class SplRPC extends SolRPC { 7 | constructor(config) { 8 | super(config); 9 | } 10 | 11 | /** 12 | * Sends a specified amount of an SPL token to a given address, either through a versioned or legacy transaction. 13 | * 14 | * @param {Object} params - The parameters for the transaction. 15 | * @param {string} params.address - The address of the SOL wallet for the recipient. 16 | * @param {number} params.amount - The amount of SPL tokens to send. 17 | * @param {SolKit.KeyPairSigner} params.fromAccountKeypair - The keypair of the sender - used as fee payer. 18 | * @param {string} [params.nonceAddress] - The public key of the nonce account 19 | * @param {string} [params.nonceAuthorityAddress] - The public key of the nonce account's authority - if not included, nonceAddress 20 | * @param {'legacy' | 0} [params.txType='legacy'] - The type of transaction ('legacy' or '0' for versioned). 21 | * @param {boolean} [params.priority=false] - Whether to add a priority fee to the transaction. 22 | * @param {string} params.mintAddress 23 | * @param {string} params.decimals 24 | * @param {string} [params.destinationAta] - The address of the ATA associated with the 'address' wallet for the provided 'mint'. If not provided, is derived - fee payer pays for address creation 25 | * @param {string} [params.sourceAta] - The address of the ATA associated with the 'fromAccountKeypair.address' wallet for the provided mint. If not provided, is derived - fee payer pays for address creation 26 | * @returns {Promise} The transaction hash. 27 | * @throws {Error} If the transaction confirmation returns an error. 28 | */ 29 | async sendToAddress({ 30 | address: addressStr, 31 | amount, 32 | fromAccountKeypair: fromAccountKeypairSigner, 33 | nonceAddress: nonceAddressStr, 34 | txType: version = 'legacy', 35 | priority, 36 | mintAddress, 37 | decimals, 38 | destinationAta, 39 | sourceAta 40 | }) { 41 | try { 42 | const VALID_TX_VERSIONS = ['legacy', 0]; 43 | if (!VALID_TX_VERSIONS.includes(version)) { 44 | throw new Error('Invalid transaction version'); 45 | } 46 | // To ensure sender's signature is in place 47 | const feePayerKeypairSigner = fromAccountKeypairSigner; 48 | 49 | const destinationAtaAddress = destinationAta ? SolKit.address(destinationAta) : await this.getOrCreateAta({ owner: SolKit.address(addressStr), mint: mintAddress, feePayer: feePayerKeypairSigner }); 50 | const sourceAtaAddress = sourceAta ? SolKit.address(sourceAta) : await this.getOrCreateAta({ owner: fromAccountKeypairSigner.address, mint: mintAddress, feePayer: feePayerKeypairSigner }); 51 | 52 | let transactionMessage = await this._createBaseTransactionMessage({ version, feePayerKeypairSigner, nonceAddressStr, priority }); 53 | transactionMessage = SolKit.appendTransactionMessageInstructions( 54 | [ 55 | SolToken.getTransferCheckedInstruction({ 56 | source: sourceAtaAddress, 57 | authority: fromAccountKeypairSigner.address, 58 | mint: SolKit.address(mintAddress), 59 | destination: destinationAtaAddress, 60 | amount, 61 | decimals 62 | }) 63 | ], 64 | transactionMessage 65 | ); 66 | const txid = await this._sendAndConfirmTransaction({ transferTransactionMessage: transactionMessage, nonceAddressStr, commitment: 'confirmed' }); 67 | return { txid, destinationAta: destinationAtaAddress, sourceAta: sourceAtaAddress }; 68 | } catch (err) { 69 | this.emitter.emit(`Failure sending a type ${version} transaction to address ${addressStr}`, err); 70 | throw err; 71 | } 72 | } 73 | 74 | /** 75 | * Retrieves an ATA address for the specified owner if it exists, or creates it 76 | * @param {Object} params 77 | * @param {SolKit.Address} params.owner 78 | * @param {string} params.mint 79 | * @param {SolKit.KeyPairSigner} params.feePayer 80 | */ 81 | async getOrCreateAta({ owner, mint, feePayer }) { 82 | const createAtaResult = await this.createAta({ ownerAddress: owner, mintAddress: mint, feePayer }); 83 | return createAtaResult.ataAddress; 84 | } 85 | 86 | /** 87 | * 88 | * @param {string} address 89 | */ 90 | async getBalance({ address }) { 91 | try { 92 | const { value } = await this.rpc.getTokenAccountBalance(address).send(); 93 | return value; 94 | } catch (err) { 95 | if (SolKit.isSolanaError) { 96 | if (err.context?.__code === -32602) { 97 | if (err.message?.toLowerCase().includes('could not find account')) { 98 | throw new Error(SOL_ERROR_MESSAGES.TOKEN_ACCOUNT_NOT_FOUND); 99 | } else if (err.message?.toLowerCase().includes('not a token account')) { 100 | throw new Error(SOL_ERROR_MESSAGES.PROVIDED_TOKEN_ADDRESS_IS_SOL); 101 | } 102 | } 103 | } 104 | throw err; 105 | } 106 | } 107 | } 108 | 109 | module.exports = SplRPC; -------------------------------------------------------------------------------- /lib/sol/error_messages.js: -------------------------------------------------------------------------------- 1 | const SOL_ERROR_MESSAGES = { 2 | ATA_NOT_INITIALIZED: 'ATA not initialized on mint for provided account. Initialize ATA first.', 3 | INVALID_MINT_PARAMETER: 'SolanaError: Invalid parameter (mint)', 4 | UNSPECIFIED_INVALID_PARAMETER: 'SolanaError: Invalid parameter (unspecified)', 5 | NON_BASE58_PARAM: 'SolanaError: Provided parameters includes non-base58 string.', 6 | TOKEN_ACCOUNT_NOT_FOUND: 'SolanaError: Account could not be found corresponding to provided address', 7 | PROVIDED_TOKEN_ADDRESS_IS_SOL: 'SolanaError: Provided address is a SOL address but should be a token address', 8 | SOL_ACCT_NOT_FOUND: 'Provided address does not correspond to an account on the Solana blockchain', 9 | ATA_ADD_SENT_INSTEAD_OF_SOL_ADD: 'SolanaError: Request object exceeds 127 bytes. This may be caused by the provided address belonging to an Associated Token Account instead of a Solana account.', 10 | }; 11 | 12 | module.exports = SOL_ERROR_MESSAGES; -------------------------------------------------------------------------------- /lib/sol/transaction-parser.js: -------------------------------------------------------------------------------- 1 | const SolComputeBudget = require('@solana-program/compute-budget'); 2 | const SolMemo = require('@solana-program/memo'); 3 | const SolSystem = require('@solana-program/system'); 4 | const SolToken = require('@solana-program/token'); 5 | const solTokenProgramAddress = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'; 6 | 7 | 8 | const instructionKeys = { 9 | TRANSFER_SOL: 'transferSol', 10 | TRANSFER_CHECKED_TOKEN: 'transferCheckedToken', 11 | TRANSFER_TOKEN: 'transferToken', 12 | ADVANCE_NONCE_ACCOUNT: 'advanceNonceAccount', 13 | MEMO: 'memo', 14 | SET_COMPUTE_UNIT_LIMIT: 'setComputeUnitLimit', 15 | SET_COMPUTE_UNIT_PRICE: 'setComputeUnitPrice', 16 | CREATE_ASSOCIATED_TOKEN: 'createAssociatedToken', 17 | CREATE_ASSOCIATED_TOKEN_IDEMPOTENT: 'createAssociatedTokenIdempotent', 18 | RECOVER_NESTED_ASSOCIATED_TOKEN: 'recoverNestedAssociatedToken', 19 | UNKNOWN: 'unknownInstruction', 20 | }; 21 | 22 | /** 23 | * @param {any[]} instructions 24 | * @returns {Instructions} 25 | */ 26 | const parseInstructions = (instructions) => { 27 | const parsedInstructions = {}; 28 | for (const instruction of instructions) { 29 | let handledInstruction; 30 | try { 31 | if (instruction.programAddress === SolSystem.SYSTEM_PROGRAM_ADDRESS) { 32 | handledInstruction = parseSystemProgramInstruction(instruction); 33 | } else if (instruction.programAddress === SolMemo.MEMO_PROGRAM_ADDRESS) { 34 | handledInstruction = parseMemoProgramInstruction(instruction); 35 | } else if (instruction.programAddress === SolToken.ASSOCIATED_TOKEN_PROGRAM_ADDRESS) { 36 | handledInstruction = parseAssociatedTokenProgramInstruction(instruction); 37 | } else if (instruction.programAddress === solTokenProgramAddress) { 38 | handledInstruction = parseTokenProgramInstruction(instruction); 39 | } else if (instruction.programAddress === SolComputeBudget.COMPUTE_BUDGET_PROGRAM_ADDRESS) { 40 | handledInstruction = parseComputeBudgetProgramInstruction(instruction); 41 | } else { 42 | handledInstruction = { 43 | key: instructionKeys.UNKNOWN, 44 | value: instruction 45 | }; 46 | } 47 | } catch (err) { 48 | handledInstruction = { 49 | key: instructionKeys.UNKNOWN, 50 | value: { 51 | ...instruction, 52 | error: err.message, 53 | programAddress: instruction.programAddress 54 | } 55 | }; 56 | } 57 | 58 | if (!parsedInstructions[handledInstruction.key]) { 59 | parsedInstructions[handledInstruction.key] = []; 60 | } 61 | parsedInstructions[handledInstruction.key].push(handledInstruction.value); 62 | } 63 | return parsedInstructions; 64 | }; 65 | 66 | const parseSystemProgramInstruction = (instruction) => { 67 | const identifiedInstruction = SolSystem.identifySystemInstruction(instruction); 68 | const parsedInstruction = {}; 69 | if (identifiedInstruction === SolSystem.SystemInstruction.TransferSol) { 70 | const parsedTransferSolInstruction = SolSystem.parseTransferSolInstruction(instruction); 71 | const { accounts, data } = parsedTransferSolInstruction; 72 | const amount = Number(data.amount); 73 | const currency = 'SOL'; 74 | const destination = accounts.destination.address; 75 | const source = accounts.source.address; 76 | parsedInstruction.key = instructionKeys.TRANSFER_SOL; 77 | parsedInstruction.value = { amount, currency, destination, source }; 78 | } else if (identifiedInstruction === SolSystem.SystemInstruction.AdvanceNonceAccount) { 79 | const parsedAdvanceNonceAccountInstruction = SolSystem.parseAdvanceNonceAccountInstruction(instruction); 80 | const { nonceAccount, nonceAuthority } = parsedAdvanceNonceAccountInstruction.accounts; 81 | parsedInstruction.key = instructionKeys.ADVANCE_NONCE_ACCOUNT, 82 | parsedInstruction.value = { 83 | nonceAccount: nonceAccount.address, 84 | nonceAuthority: nonceAuthority.address 85 | }; 86 | } else { 87 | parsedInstruction.key = `unparsedSystemInstruction_${identifiedInstruction}`; 88 | parsedInstruction.value = instruction; 89 | } 90 | return parsedInstruction; 91 | }; 92 | 93 | const parseMemoProgramInstruction = (instruction) => { 94 | const parsedInstruction = {}; 95 | const parsedMemoInstruction = SolMemo.parseAddMemoInstruction({...instruction}); // Only one instruction with MEMO_PROGRAM_ADDRESS 96 | parsedInstruction.key = instructionKeys.MEMO; 97 | parsedInstruction.value = parsedMemoInstruction.data; 98 | return parsedInstruction; 99 | }; 100 | 101 | const parseAssociatedTokenProgramInstruction = (instruction) => { 102 | // Add data field if missing 103 | let processedInstruction = instruction; 104 | if (!instruction.data) { 105 | // Default to CreateAssociatedToken instruction (most common) 106 | processedInstruction = { 107 | ...instruction, 108 | data: new Uint8Array([SolToken.AssociatedTokenInstruction.CreateAssociatedToken]) // CreateAssociatedToken discriminator 109 | }; 110 | } 111 | 112 | const identifiedTokenInstruction = SolToken.identifyAssociatedTokenInstruction(processedInstruction); 113 | 114 | if (identifiedTokenInstruction === SolToken.AssociatedTokenInstruction.CreateAssociatedToken) { 115 | const parsedCreateInstruction = SolToken.parseCreateAssociatedTokenInstruction(processedInstruction); 116 | const { accounts } = parsedCreateInstruction; 117 | 118 | return { 119 | key: instructionKeys.CREATE_ASSOCIATED_TOKEN, 120 | value: { 121 | payer: accounts.payer.address, 122 | associatedTokenAccount: accounts.ata.address, 123 | owner: accounts.owner.address, 124 | mint: accounts.mint.address, 125 | tokenProgram: accounts.tokenProgram.address 126 | } 127 | }; 128 | } else if (identifiedTokenInstruction === SolToken.AssociatedTokenInstruction.CreateAssociatedTokenIdempotent) { 129 | const parsedIdempotentInstruction = SolToken.parseCreateAssociatedTokenIdempotentInstruction(processedInstruction); 130 | const { accounts } = parsedIdempotentInstruction; 131 | 132 | return { 133 | key: instructionKeys.CREATE_ASSOCIATED_TOKEN_IDEMPOTENT, 134 | value: { 135 | payer: accounts.payer.address, 136 | associatedTokenAccount: accounts.ata.address, 137 | owner: accounts.owner.address, 138 | mint: accounts.mint.address, 139 | tokenProgram: accounts.tokenProgram.address 140 | } 141 | }; 142 | } else if (identifiedTokenInstruction === SolToken.AssociatedTokenInstruction.RecoverNestedAssociatedToken) { 143 | const parsedRecoverInstruction = SolToken.parseRecoverNestedAssociatedTokenInstruction(processedInstruction); 144 | const { accounts } = parsedRecoverInstruction; 145 | 146 | return { 147 | key: instructionKeys.RECOVER_NESTED_ASSOCIATED_TOKEN, 148 | value: { 149 | nestedAssociatedTokenAccount: accounts.nestedAta.address, 150 | nestedMint: accounts.nestedMint.address, 151 | destinationAssociatedTokenAccount: accounts.destinationAta.address, 152 | owner: accounts.owner.address, 153 | mint: accounts.mint.address, 154 | tokenProgram: accounts.tokenProgram.address 155 | } 156 | }; 157 | } else { 158 | return { 159 | key: `unparsedAssociatedTokenInstruction_${identifiedTokenInstruction}`, 160 | value: instruction 161 | }; 162 | } 163 | }; 164 | 165 | const parseTokenProgramInstruction = (instruction) => { 166 | const parsedInstruction = {}; 167 | const identifiedTokenInstruction = SolToken.identifyTokenInstruction(instruction); 168 | if (identifiedTokenInstruction === SolToken.TokenInstruction.Transfer) { 169 | const parsedTransferTokenInstruction = SolToken.parseTransferInstruction(instruction); 170 | const { accounts, data } = parsedTransferTokenInstruction; 171 | parsedInstruction.key = instructionKeys.TRANSFER_TOKEN; 172 | parsedInstruction.value = { 173 | authority: accounts.authority.address, 174 | destination: accounts.destination.address, 175 | source: accounts.source.address, 176 | amount: Number(data.amount) 177 | }; 178 | } else if (identifiedTokenInstruction === SolToken.TokenInstruction.TransferChecked) { 179 | const parsedTransferCheckedTokenInstruction = SolToken.parseTransferCheckedInstruction(instruction); 180 | const { accounts, data } = parsedTransferCheckedTokenInstruction; 181 | parsedInstruction.key = instructionKeys.TRANSFER_CHECKED_TOKEN; 182 | parsedInstruction.value = { 183 | authority: accounts.authority.address, 184 | destination: accounts.destination.address, 185 | mint: accounts.mint.address, 186 | source: accounts.source.address, 187 | amount: Number(data.amount), 188 | decimals: data.decimals 189 | }; 190 | } else { 191 | parsedInstruction.key = `unparsedTokenInstruction_${identifiedTokenInstruction}`; 192 | parsedInstruction.value = instruction; 193 | } 194 | return parsedInstruction; 195 | }; 196 | 197 | const parseComputeBudgetProgramInstruction = (instruction) => { 198 | const parsedInstruction = {}; 199 | const identifiedComputeBudgetInstruction = SolComputeBudget.identifyComputeBudgetInstruction(instruction); 200 | if (identifiedComputeBudgetInstruction === SolComputeBudget.ComputeBudgetInstruction.SetComputeUnitLimit) { 201 | const parsedSetComputeUnitLimitInstruction = SolComputeBudget.parseSetComputeUnitLimitInstruction(instruction); 202 | parsedInstruction.key = instructionKeys.SET_COMPUTE_UNIT_LIMIT; 203 | parsedInstruction.value = { 204 | computeUnitLimit: parsedSetComputeUnitLimitInstruction.data.units 205 | }; 206 | } else if (identifiedComputeBudgetInstruction === SolComputeBudget.ComputeBudgetInstruction.SetComputeUnitPrice) { 207 | const parsedSetComputeUnitPriceInstruction = SolComputeBudget.parseSetComputeUnitPriceInstruction(instruction); 208 | parsedInstruction.key = instructionKeys.SET_COMPUTE_UNIT_PRICE; 209 | parsedInstruction.value = { 210 | priority: true, 211 | microLamports: Number(parsedSetComputeUnitPriceInstruction.data.microLamports) 212 | }; 213 | } else { 214 | parsedInstruction.key = `unparsedComputeBudgetInstruction_${identifiedComputeBudgetInstruction}`; 215 | parsedInstruction.value = instruction; 216 | } 217 | return parsedInstruction; 218 | }; 219 | 220 | module.exports = { 221 | parseInstructions, 222 | instructionKeys 223 | }; 224 | 225 | /** 226 | * @typedef {Object} TransferSolInstruction 227 | * @property {number} amount - Amount of SOL to transfer in lamports 228 | * @property {'SOL'} currency - Currency type, always "SOL" 229 | * @property {string} destination - Destination wallet address 230 | * @property {string} source - Source wallet address 231 | */ 232 | 233 | /** 234 | * @typedef {Object} AdvanceNonceAccountInstruction 235 | * @property {string} nonceAccount - Address of the nonce account 236 | * @property {string} nonceAuthority - Address of the nonce authority 237 | */ 238 | 239 | /** 240 | * @typedef {Object} SetComputeUnitLimitInstruction 241 | * @property {number} computeUnitLimit - Maximum compute units allowed 242 | */ 243 | 244 | /** 245 | * @typedef {Object} SetComputeUnitPriceInstruction 246 | * @property {true} priority - Whether this is a priority transaction 247 | * @property {number} microLamports - Price in micro-lamports per compute unit 248 | */ 249 | 250 | /** 251 | * @typedef {Object} MemoInstruction 252 | * @property {string} memo - Memo text content 253 | */ 254 | 255 | /** 256 | * @typedef {Object} TransferCheckedTokenInstruction 257 | * @property {number} amount 258 | * @property {string} authority 259 | * @property {number} decimals 260 | * @property {string} destination 261 | * @property {string} mint 262 | * @property {string} source 263 | */ 264 | 265 | /** 266 | * @typedef {Object} TransferTokenInstruction 267 | * @property {number} amount 268 | * @property {string} authority 269 | * @property {string} destination 270 | * @property {string} source 271 | */ 272 | 273 | /** 274 | * May also include additional unknown instructions 275 | * @typedef {Object} Instructions 276 | * @property {TransferSolInstruction[]} [transferSol] 277 | * @property {AdvanceNonceAccountInstruction[]} [advanceNonceAccount] 278 | * @property {SetComputeUnitLimitInstruction[]} [setComputeUnitLimit] 279 | * @property {SetComputeUnitPriceInstruction[]} [setComputeUnitPrice] 280 | * @property {MemoInstruction[]} [memo] 281 | * @property {TransferCheckedTokenInstruction[]} [transferCheckedToken] 282 | * @property {TransferTokenInstruction[]} [transferToken] 283 | */ -------------------------------------------------------------------------------- /lib/xrp/XrpClientAdapter.js: -------------------------------------------------------------------------------- 1 | const xrpl = require('xrpl'); 2 | 3 | // Array elements should be lower-case 4 | const SUPPORTED_TRANSACTION_TYPES = new Set(['payment']); 5 | // https://xrpl.org/docs/references/protocol/transactions/transaction-results 6 | const SUPPORTED_TRANSACTION_RESULT_CODES = { 7 | tes: { 8 | SUCCESS: 'tesSUCCESS' 9 | } 10 | }; 11 | 12 | /** 13 | * This adapter is used to adapt the new XRP client provided by the xrpl v2 dependency 14 | * so that it will behave like the old XRP client provided by ripple-lib v1 15 | * 16 | * Migration guide: https://xrpl.org/docs/references/xrpljs2-migration-guide 17 | * ripple-lib ref: https://github.com/XRPLF/xrpl.js/blob/1.x/docs/index.md 18 | */ 19 | class XrpClientAdapter extends xrpl.Client { 20 | async getServerInfo() { 21 | return await this.request({ 22 | id: 1, 23 | command: 'server_info' 24 | }); 25 | } 26 | 27 | async getLedger({ ledgerVersion }) { 28 | return await this.request({ 29 | command: 'ledger', 30 | ledger_index: ledgerVersion 31 | }); 32 | } 33 | 34 | /** 35 | * Retrieves transactions based on the provided parameters. 36 | * 37 | * @param {string} acceptanceAddress - The address of the account to get transactions for. 38 | * @param {Object} options - The options for retrieving transactions. 39 | * @param {number} [options.minLedgerVersion] - Return only transactions in this ledger version or higher. 40 | * @param {number} [options.maxLedgerVersion] - Return only transactions in this ledger version or lower. 41 | * @param {Array} [options.types] - Only return transactions of the specified valid Transaction Types (see SUPPORTED_TRANSACTION_TYPES). 42 | * @param {boolean} [options.initiated] - If true, return only transactions initiated by the account specified by acceptanceAddress. If false, return only transactions NOT initiated by the account specified by acceptanceAddress. 43 | * @param {boolean} [options.includeRawTransactions] - Include raw transaction data. For advanced users; exercise caution when interpreting this data. 44 | * @param {boolean} [options.excludeFailures] - If true, the result omits transactions that did not succeed. 45 | * @returns {Promise} A promise that resolves to an array of transactions. 46 | * 47 | * @throws {Error} If 'includeRawTransactions' is set to false. 48 | * @throws {Error} If 'types' is included but not an array. 49 | * @throws {Error} If 'types' is included but empty. 50 | * @throws {Error} If 'types' includes invalid transaction types. 51 | * @throws {Error} If the XRPL client request does not return the expected form. 52 | */ 53 | async getTransactions(acceptanceAddress, { 54 | minLedgerVersion, 55 | maxLedgerVersion, 56 | types, 57 | initiated, 58 | includeRawTransactions, 59 | excludeFailures 60 | }) { 61 | /** 62 | * Behavior defaults to 'true', but this error is to document that 'includeRawTransactions: false' is NOT supported 63 | * Truthiness is not sufficient for this check - it must explicitly be an equality check, & strict equality is prefered 64 | */ 65 | if (includeRawTransactions === false) { 66 | throw new Error('"includeRawTransactions: false" not supported'); 67 | } 68 | 69 | /** 70 | * Filtering constants with defaults 71 | */ 72 | let TYPES = SUPPORTED_TRANSACTION_TYPES; 73 | if (types) { 74 | if (!Array.isArray(types)) throw new Error('If types is included, it should be a string array of supported types. See documentation for usage.'); 75 | if (types.length === 0) throw new Error('If types is included, it should include at least one supported type'); 76 | 77 | const validTypes = new Set(); 78 | const invalidTypes = []; 79 | types.forEach(type => { 80 | const lowercaseType = type.toLowerCase(); 81 | if (SUPPORTED_TRANSACTION_TYPES.has(lowercaseType)) { 82 | validTypes.add(lowercaseType); 83 | } else { 84 | invalidTypes.push(type); 85 | } 86 | }); 87 | 88 | if (invalidTypes.length > 0) throw new Error(`Invalid types included: ${invalidTypes.join(', ')}`); 89 | 90 | TYPES = validTypes; 91 | } 92 | 93 | // Boolean option checks must be checked against type for existence instead of using fallback assignment 94 | const INITIATED = typeof initiated === 'boolean' ? initiated : false; 95 | const EXCLUDE_FAILURES = typeof excludeFailures === 'boolean' ? excludeFailures : true; 96 | const INCLUDE_RAW_TRANSACTIONS = typeof includeRawTransactions === 'boolean' ? includeRawTransactions : true; 97 | 98 | const { result } = await this.request({ 99 | command: 'account_tx', 100 | account: acceptanceAddress, 101 | ledger_index_min: minLedgerVersion, 102 | ledger_index_max: maxLedgerVersion 103 | }); 104 | 105 | if (!(result && Array.isArray(result.transactions))) { 106 | throw new Error('xrpl client request did not return expected form'); 107 | } 108 | 109 | const filteredTransactions = result.transactions.filter(({ meta, tx }) => { 110 | const { Account: initiatingAccount, TransactionType } = tx; 111 | 112 | if (!TYPES.has(TransactionType.toLowerCase())) return false; 113 | 114 | const isTxInitiatedByAcceptanceAddress = initiatingAccount === acceptanceAddress; 115 | /** 116 | * INITIATED 117 | * If true, return only transactions initiated by the account specified by acceptanceAddress. 118 | * If false, return only transactions NOT initiated by account specified by acceptanceAddress 119 | * 120 | * Logical XOR 121 | */ 122 | if (INITIATED !== isTxInitiatedByAcceptanceAddress) return false; 123 | 124 | if (EXCLUDE_FAILURES && meta.TransactionResult !== SUPPORTED_TRANSACTION_RESULT_CODES.tes.SUCCESS) return false; 125 | 126 | /** 127 | * If type in types AND tx initiator matches flag AND if excludeFailures, only include successes 128 | */ 129 | return true; 130 | }); 131 | 132 | // Only 'INCLUDE_RAW_TRANSACTIONS: true' is supported - this case is here for future expansion 133 | if (!INCLUDE_RAW_TRANSACTIONS) { 134 | return filteredTransactions; 135 | } 136 | 137 | return filteredTransactions.map(({ meta, tx, validated }) => { 138 | // ! NOTE ! The raw transaction is missing the 'DeliverMax' property & adds 'LastLedgerSequence' property 139 | const mappedRawTransaction = { 140 | ...tx, 141 | meta, 142 | validated 143 | }; 144 | 145 | // Only rawTransaction used - other transaction properties excluded for simplicity. May be added as required. 146 | return { 147 | rawTransaction: JSON.stringify(mappedRawTransaction) 148 | }; 149 | }); 150 | } 151 | } 152 | 153 | module.exports = XrpClientAdapter; 154 | 155 | // Find examples of response at https://xrpl.org/docs/references/http-websocket-apis/public-api-methods/account-methods/account_tx -------------------------------------------------------------------------------- /lib/xrp/XrpRpc.js: -------------------------------------------------------------------------------- 1 | const xrpl = require('xrpl'); 2 | const XrpClientAdapter = require('./XrpClientAdapter'); 3 | const promptly = require('promptly'); 4 | const util = require('util'); 5 | const EventEmitter = require('events'); 6 | 7 | const passwordPromptAsync = util.promisify(promptly.password); 8 | 9 | class XrpRpc { 10 | constructor(config) { 11 | this.config = config; 12 | const { 13 | rpcPort, 14 | host, 15 | protocol, 16 | address, 17 | useClientAdapter = true 18 | } = config; 19 | const connectionString = `${protocol}://${host}:${rpcPort}`; 20 | // Use client adapter to include implementations of ripple-lib Client methods as an extension to the xrpl Client 21 | this.rpc = useClientAdapter ? new XrpClientAdapter(connectionString) : new xrpl.Client(connectionString); 22 | this.address = address; 23 | this.emitter = new EventEmitter(); 24 | this.connectionIdleTimeout = null; 25 | this.connectionIdleMs = config.connectionIdleMs || 120000; 26 | this.rpc.on('error', () => {}); // ignore rpc connection errors as we reconnect if nec each request 27 | } 28 | 29 | async asyncCall(method, args) { 30 | // clear idle timer if exists 31 | clearTimeout(this.connectionIdleTimeout); 32 | // reset idle timer 33 | this.connectionIdleTimeout = setTimeout(async () => { 34 | try { 35 | await this.rpc.disconnect(); 36 | } catch (_) { 37 | // ignore disconnection error on idle 38 | } 39 | }, this.connectionIdleMs); 40 | this.connectionIdleTimeout.unref(); 41 | 42 | if (!this.rpc.isConnected()) { 43 | // if there is an error connecting, throw error and try again on next call 44 | await this.rpc.connect(); 45 | } 46 | 47 | let result; 48 | if (!Array.isArray(args)) args = [args]; 49 | result = await this.rpc[method](...args); 50 | return result; 51 | } 52 | 53 | async asyncRequest(method, args) { 54 | return this.asyncCall('request', Object.assign({ command: method }, args)); 55 | } 56 | 57 | async unlockAndSendToAddress({ address, amount, secret }) { 58 | if (secret === undefined) { 59 | secret = await passwordPromptAsync('> '); 60 | } 61 | console.warn('Unlocking for a single transaction.'); 62 | return await this.sendToAddress({ address, amount, secret }); 63 | } 64 | 65 | async sendToAddress({ address, amount, passphrase, tag, invoiceID, secret }) { 66 | let rawTx = await this.signTransaction({ address, amount, passphrase, tag, invoiceID, secret }); 67 | let txHash = await this.sendRawTransaction({ rawTx }); 68 | return txHash; 69 | } 70 | 71 | async signTransaction({ address, amount, tag, invoiceID, secret }) { 72 | if (!secret) { 73 | const err = new Error('Secret not provided'); 74 | err.conclusive = true; // used by server 75 | throw err; 76 | } 77 | let wallet = xrpl.Wallet.fromSeed(secret); 78 | let payment = { 79 | TransactionType: 'Payment', 80 | Account: wallet.address, 81 | Amount: xrpl.xrpToDrops(amount), 82 | Destination: address 83 | }; 84 | if (tag) { 85 | payment.DestinationTag = tag; 86 | } 87 | if (invoiceID) { 88 | payment.InvoiceID = invoiceID; 89 | } 90 | const prepared = await this.asyncCall('autofill', payment); 91 | const signed = wallet.sign(prepared); 92 | return signed.tx_blob; 93 | } 94 | 95 | async sendRawTransactionMany({ rawTxArray }) { 96 | let resultArray = []; 97 | for (const rawTx of rawTxArray) { 98 | const emitData = { rawTx }; 99 | try { 100 | let txHash = await this.sendRawTransaction({ rawTx }); 101 | emitData.txid = txHash; 102 | resultArray.push(emitData); 103 | this.emitter.emit('success', emitData); 104 | } catch (e) { 105 | emitData.error = e; 106 | resultArray.push(emitData); 107 | this.emitter.emit('failure', emitData); 108 | } 109 | } 110 | return resultArray; 111 | } 112 | 113 | async unlockAndSendToAddressMany({ payToArray, secret }) { 114 | if (secret === undefined) { 115 | secret = await passwordPromptAsync('> '); 116 | } 117 | 118 | const resultArray = []; 119 | 120 | for (const payment of payToArray) { 121 | const { address, amount, id } = payment; 122 | const emitData = { address, amount, id }; 123 | this.emitter.emit('attempt', emitData); 124 | try { 125 | const txid = await this.sendToAddress({ address, amount, secret }); 126 | emitData.txid = txid; 127 | resultArray.push(emitData); 128 | this.emitter.emit('success', emitData); 129 | 130 | // do not await confirmations, the submitted txs are pending not confirmed 131 | } catch (e) { 132 | emitData.error = e; 133 | resultArray.push(emitData); 134 | this.emitter.emit('failure', emitData); 135 | } 136 | } 137 | return resultArray; 138 | } 139 | 140 | 141 | async getRawTransaction({ txid }) { 142 | try { 143 | const { result } = await this.asyncRequest('tx', { transaction: txid, binary: true }); 144 | return result.tx; 145 | } catch (err) { 146 | if (err && err.data && err.data.error === 'txnNotFound') { 147 | return null; 148 | } 149 | throw err; 150 | } 151 | } 152 | 153 | async sendRawTransaction({ rawTx }) { 154 | const { result } = await this.asyncCall('submit', rawTx); 155 | const { accepted, engine_result_message, tx_json } = result; 156 | if (accepted) { 157 | return tx_json.hash; 158 | } else { 159 | throw new Error(engine_result_message); 160 | } 161 | } 162 | 163 | async decodeRawTransaction({ rawTx }) { 164 | const txJSON = xrpl.decode(rawTx); 165 | 166 | if (txJSON.TxnSignature) { 167 | txJSON.hash = xrpl.hashes.hashSignedTx(rawTx); 168 | } 169 | 170 | return txJSON; 171 | } 172 | 173 | async estimateFee() { 174 | const { result } = await this.asyncRequest('fee'); 175 | return result.drops.minimum_fee; 176 | } 177 | 178 | async getBalance({ address, ledgerIndex }) { 179 | let balance = await this.asyncCall('getXrpBalance', [address || this.address, { ledger_index: ledgerIndex }]); 180 | return parseFloat(balance); 181 | } 182 | 183 | async getBestBlockHash() { 184 | let tip = await this.getTip(); 185 | return tip.hash; 186 | } 187 | 188 | async getTransaction({ txid }) { 189 | try { 190 | const { result } = await this.asyncRequest('tx', { transaction: txid }); 191 | if (!result) { 192 | return null; 193 | } 194 | // Append Confirmations 195 | if (!result.ledger_index) { 196 | result.confirmations = 0; 197 | } else { 198 | let tip = await this.getTip(); 199 | let height = tip.height; 200 | result.confirmations = height - result.ledger_index + 1; // Tip is considered confirmed 201 | } 202 | // Append BlockHash 203 | const { result: txBlock } = await this.asyncRequest('ledger', { ledger_index: result.ledger_index }); 204 | result.blockHash = txBlock.ledger_hash; 205 | return result; 206 | } catch (err) { 207 | if (err && err.data && err.data.error === 'txnNotFound') { 208 | return null; 209 | } 210 | throw err; 211 | } 212 | } 213 | 214 | /** 215 | * Get all transactions for an account 216 | * @param {string} address Account to get transactions for 217 | * @param {object} options See https://xrpl.org/docs/references/http-websocket-apis/public-api-methods/account-methods/account_tx/ 218 | * @returns 219 | */ 220 | async getTransactions({ address, options }) { 221 | try { 222 | const { result } = await this.asyncRequest('account_tx', { account: address, ...options }); 223 | return result; 224 | } catch (err) { 225 | if (err && err.data && err.data.error === 'actNotFound') { 226 | return []; 227 | } 228 | throw err; 229 | } 230 | } 231 | 232 | async getBlock({ hash, index, transactions = true, expand = true } = {}) { 233 | try { 234 | if (index === 'latest') { 235 | index = 'validated'; 236 | } 237 | let result; 238 | if (hash) { 239 | ({ result } = await this._getBlockByHash({ hash, transactions, expand })); 240 | } else { 241 | ({ result } = await this._getBlockByIndex({ index, transactions, expand })); 242 | } 243 | return result; 244 | } catch (err) { 245 | if (err && err.data && err.data.error === 'lgrNotFound') { 246 | return null; 247 | } 248 | throw err; 249 | } 250 | } 251 | 252 | _getBlockByHash({ hash, transactions = true, expand = true }) { 253 | return this.asyncRequest('ledger', { 254 | ledger_hash: hash, 255 | transactions, 256 | expand 257 | }); 258 | } 259 | 260 | _getBlockByIndex({ index = 'validated', transactions = true, expand = true }) { 261 | return this.asyncRequest('ledger', { 262 | ledger_index: index, 263 | transactions, 264 | expand 265 | }); 266 | } 267 | 268 | async getConfirmations({ txid }) { 269 | return (await this.getTransaction({ txid })).confirmations; 270 | } 271 | 272 | async getTip() { 273 | const { result } = await this.asyncRequest('ledger', { 274 | ledger_index: 'validated' 275 | }); 276 | let height = result.ledger_index; 277 | let hash = result.ledger_hash; 278 | return { height, hash }; 279 | } 280 | 281 | async getTxOutputInfo() { 282 | return null; 283 | } 284 | 285 | async validateAddress({ address }) { 286 | return xrpl.isValidAddress(address); 287 | } 288 | 289 | async getAccountInfo({ address }) { 290 | try { 291 | const { result } = await this.asyncRequest('account_info', { account: address }); 292 | return result; 293 | } catch (error) { 294 | if (error.data && error.data.error && error.data.error === 'actNotFound') { 295 | error.conclusive = true; 296 | } 297 | throw error; 298 | } 299 | } 300 | 301 | async getServerInfo() { 302 | const { result } = await this.asyncRequest('server_info'); 303 | return result.info; 304 | } 305 | } 306 | 307 | module.exports = XrpRpc; 308 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-rpc", 3 | "version": "1.1.0", 4 | "description": "rpc wrapper for multiple rpcs", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "docker compose build", 8 | "start": "npm run stop && npm run build && docker compose run start", 9 | "restart": "npm run stop && docker compose run start", 10 | "stop": "docker compose down", 11 | "compile": "npm run lint && npm run truffle:compile", 12 | "migrate": "npm run compile && npm run truffle:migrate", 13 | "lint": "npx eslint .", 14 | "test": "npm run test:ci", 15 | "test:ci": "docker-compose down && docker-compose build && docker-compose run test_runner", 16 | "test:local": "npm run stop && npm run build && docker compose run test_runner", 17 | "docker:test": "npm run migrate && npm run truffle:test && mocha --recursive ./tests", 18 | "truffle:compile": "cd blockchain/EVM/ && ../../node_modules/.bin/truffle compile --network development_geth && ../../node_modules/.bin/truffle compile --network development_matic", 19 | "truffle:test": "cd blockchain/EVM/ && ../../node_modules/.bin/truffle test --network development_geth && ../../node_modules/.bin/truffle test --network development_matic", 20 | "truffle:migrate": "cd blockchain/EVM/ && ../../node_modules/.bin/truffle migrate --network development_geth && ../../node_modules/.bin/truffle migrate --network development_matic" 21 | }, 22 | "author": "Micah Riggan", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@solana-program/address-lookup-table": "^0.7.0", 26 | "@solana-program/compute-budget": "^0.7.0", 27 | "@solana-program/memo": "^0.7.0", 28 | "@solana-program/system": "^0.7.0", 29 | "@solana-program/token": "^0.5.1", 30 | "@solana/kit": "^2.1.0", 31 | "bitcoind-rpc": "0.9.1", 32 | "commander": "2.8.1", 33 | "dogecoind-rpc": "0.8.1", 34 | "ethers": "5.7.2", 35 | "lightning": "10.0.1", 36 | "promptly": "0.2.0", 37 | "web3": "1.7.1", 38 | "xrpl": "^2.6.0" 39 | }, 40 | "devDependencies": { 41 | "assert": "^1.4.1", 42 | "chai": "^4.2.0", 43 | "eslint": "^8.57.0", 44 | "eslint-config-standard": "^17.1.0", 45 | "eslint-plugin-import": "^2.29.1", 46 | "eslint-plugin-node": "^11.1.0", 47 | "eslint-plugin-promise": "^6.6.0", 48 | "eslint-plugin-standard": "^5.0.0", 49 | "mocha": "^5.2.0", 50 | "sinon": "^7.3.1", 51 | "truffle": "5.1.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /start.dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 2 | WORKDIR /crypto-rpc 3 | COPY package.json . 4 | RUN mkdir ~/.ssh 5 | RUN ssh-keyscan github.com >> ~/.ssh/known_hosts 6 | RUN npm install 7 | ADD . . 8 | ENV PATH="/crypto-rpc/tests/docker/solc-v0.4.24:${PATH}" 9 | CMD ["npm", "run", "migrate"] -------------------------------------------------------------------------------- /tests/bch.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const sinon = require('sinon'); 3 | const assert = require('assert'); 4 | const mocha = require('mocha'); 5 | const { expect } = require('chai'); 6 | const { before, describe, it } = mocha; 7 | const config = { 8 | chain: 'BCH', 9 | host: process.env.HOST_BCH || 'bitcoin-cash', 10 | protocol: 'http', 11 | rpcPort: '9333', 12 | rpcUser: 'cryptorpc', 13 | rpcPass: 'local321', 14 | tokens: {}, 15 | currencyConfig: { 16 | sendTo: 'bchreg:qq9kqhzxeul20r7nsl2lrwh8d5kw97np9u960ue086', 17 | unlockPassword: 'password', 18 | rawTx: 19 | '0200000001445703d7470ec3e435db0f33da332fc654ae0c8d264572e487bd427125659d7500000000484730440220704a6a336eb930a95b2a6a941b3c43ccb2207db803a2332512ac255c1740b9d7022057c7bc00a188de7f4868774d1e9ff626f8bd6eca8187763b9cb184354ddc5dde41feffffff0200021024010000001976a914db1f764e6a60e4a8cb919c55e95ac41517f5cddc88ac00e1f505000000001976a9140b605c46cf3ea78fd387d5f1bae76d2ce2fa612f88ac66000000' 20 | } 21 | }; 22 | 23 | describe('BCH Tests', function() { 24 | this.timeout(30000); 25 | let blockHash = ''; 26 | const currency = 'BCH'; 27 | const { currencyConfig } = config; 28 | const rpcs = new CryptoRpc(config, currencyConfig); 29 | const bitcoin = rpcs.get(currency); 30 | 31 | before(async () => { 32 | try { 33 | await bitcoin.asyncCall('encryptWallet', ['password']); 34 | } catch (e) { 35 | console.warn('wallet already encrypted'); 36 | } 37 | await new Promise(resolve => setTimeout(resolve, 2000)); 38 | await bitcoin.asyncCall('generate', [101]); 39 | }); 40 | 41 | 42 | it('should be able to get a block hash', async () => { 43 | blockHash = await rpcs.getBestBlockHash({ currency }); 44 | expect(blockHash).to.have.lengthOf('64'); 45 | }); 46 | 47 | 48 | it('should convert fee to satoshis per kilobyte with estimateFee', async () => { 49 | sinon.stub(bitcoin.rpc,'estimateFee').callsFake((cb) => { 50 | cb(null, {result: 0.00001234}); 51 | }); 52 | const fee = await bitcoin.estimateFee(); 53 | expect(fee).to.be.eq(1.234); 54 | }); 55 | 56 | it('should get block', async () => { 57 | const reqBlock = await rpcs.getBlock({ currency, hash: blockHash }); 58 | expect(reqBlock).to.have.property('hash'); 59 | expect(reqBlock).to.have.property('confirmations'); 60 | expect(reqBlock).to.have.property('size'); 61 | expect(reqBlock).to.have.property('height'); 62 | expect(reqBlock).to.have.property('version'); 63 | expect(reqBlock).to.have.property('versionHex'); 64 | expect(reqBlock).to.have.property('merkleroot'); 65 | expect(reqBlock).to.have.property('tx'); 66 | expect(reqBlock).to.have.property('time'); 67 | expect(reqBlock).to.have.property('mediantime'); 68 | expect(reqBlock).to.have.property('nonce'); 69 | expect(reqBlock).to.have.property('bits'); 70 | expect(reqBlock).to.have.property('difficulty'); 71 | expect(reqBlock).to.have.property('chainwork'); 72 | expect(reqBlock).to.have.property('previousblockhash'); 73 | assert(reqBlock); 74 | }); 75 | 76 | it('should be able to get a balance', async () => { 77 | const balance = await rpcs.getBalance({ currency }); 78 | expect(balance).to.eq(5000000000); 79 | assert(balance != undefined); 80 | }); 81 | 82 | it('should be able to send a transaction', async () => { 83 | const txid = await rpcs.unlockAndSendToAddress({ currency, address: config.currencyConfig.sendTo, amount: '10000', passphrase: currencyConfig.unlockPassword }); 84 | expect(txid).to.have.lengthOf(64); 85 | assert(txid); 86 | await bitcoin.asyncCall('generate', [2]); 87 | 88 | it('should get confirmations', async () => { 89 | let confirmations = await rpcs.getConfirmations({ currency, txid }); 90 | assert(confirmations != undefined); 91 | expect(confirmations).to.eq(2); 92 | }); 93 | 94 | 95 | it('should be able to get a transaction', async () => { 96 | const tx = await rpcs.getTransaction({ currency, txid }); 97 | expect(tx).to.have.property('txid'); 98 | expect(tx).to.have.property('hash'); 99 | expect(tx).to.have.property('version'); 100 | expect(tx).to.have.property('size'); 101 | expect(tx).to.have.property('locktime'); 102 | expect(tx).to.have.property('vin'); 103 | expect(tx).to.have.property('vout'); 104 | expect(tx).to.have.property('hex'); 105 | assert(tx); 106 | assert(typeof tx === 'object'); 107 | }); 108 | }); 109 | 110 | it('should be able to send many transactions', async () => { 111 | let payToArray = []; 112 | let transaction1 = { 113 | address: 'bchreg:qrmap3fwpufpzk8j936aetfupppezngfeut6kqqds6', 114 | amount: 10000 115 | }; 116 | let transaction2 = { 117 | address: 'bchreg:qpmrahuqhpmq4se34zx4lt9lp3l5j4t4ggzf98lk8v', 118 | amount: 20000 119 | }; 120 | let transaction3 = { 121 | address: 'qz07vf90w70s8d0pfx9qygxxlpgr2vwz65d53p22cr', 122 | amount: 30000 123 | }; 124 | let transaction4 = { 125 | address: 'qzp2lmc7m49du2n55qmyattncf404vmgnq8gr53aj7', 126 | amount: 40000 127 | }; 128 | payToArray.push(transaction1); 129 | payToArray.push(transaction2); 130 | payToArray.push(transaction3); 131 | payToArray.push(transaction4); 132 | const maxOutputs = 2; 133 | const maxValue = 1e8; 134 | const eventEmitter = rpcs.rpcs.BCH.emitter; 135 | let eventCounter = 0; 136 | let emitResults = []; 137 | const emitPromise = new Promise(resolve => { 138 | eventEmitter.on('success', (emitData) => { 139 | eventCounter++; 140 | emitResults.push(emitData); 141 | if (eventCounter === 3) { 142 | resolve(); 143 | } 144 | }); 145 | }); 146 | const outputArray = await rpcs.unlockAndSendToAddressMany({ payToArray, passphrase: currencyConfig.unlockPassword, time: 1000, maxValue, maxOutputs }); 147 | await emitPromise; 148 | expect(outputArray).to.have.lengthOf(4); 149 | expect(outputArray[0].txid).to.equal(outputArray[1].txid); 150 | expect(outputArray[2].txid).to.equal(outputArray[3].txid); 151 | expect(outputArray[1].txid).to.not.equal(outputArray[2].txid); 152 | for (let transaction of outputArray) { 153 | assert(transaction.txid); 154 | expect(transaction.txid).to.have.lengthOf(64); 155 | } 156 | for (let emitData of emitResults) { 157 | assert(emitData.address); 158 | assert(emitData.amount); 159 | assert(emitData.txid); 160 | expect(emitData.error === null); 161 | expect(emitData.vout === 0 || emitData.vout === 1); 162 | let transactionObj = {address: emitData.address, amount: emitData.amount}; 163 | expect(payToArray.includes(transactionObj)); 164 | } 165 | 166 | await bitcoin.asyncCall('generate', [10]); 167 | }); 168 | 169 | it('should reject when one of many transactions fails', async () => { 170 | const payToArray = [ 171 | { address: 'bchreg:qrmap3fwpufpzk8j936aetfupppezngfeut6kqqds6', 172 | amount: 10000 173 | }, 174 | { address: 'funkyColdMedina', 175 | amount: 1 176 | }, 177 | ]; 178 | const eventEmitter = rpcs.rpcs.BCH.emitter; 179 | let emitResults = []; 180 | const emitPromise = new Promise(resolve => { 181 | eventEmitter.on('failure', (emitData) => { 182 | emitResults.push(emitData); 183 | resolve(); 184 | }); 185 | }); 186 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 187 | currency, 188 | payToArray, 189 | passphrase: currencyConfig.unlockPassword 190 | }); 191 | await emitPromise; 192 | assert(!outputArray[1].txid); 193 | expect(outputArray[1].error).to.equal(emitResults[0].error); 194 | expect(emitResults.length).to.equal(1); 195 | assert(emitResults[0].error); 196 | }); 197 | 198 | 199 | it('should be able to decode a raw transaction', async () => { 200 | const { rawTx } = config.currencyConfig; 201 | assert(rawTx); 202 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 203 | expect(decoded).to.have.property('txid'); 204 | expect(decoded).to.have.property('hash'); 205 | expect(decoded).to.have.property('version'); 206 | expect(decoded).to.have.property('size'); 207 | expect(decoded).to.have.property('locktime'); 208 | expect(decoded).to.have.property('vin'); 209 | expect(decoded).to.have.property('vout'); 210 | assert(decoded); 211 | }); 212 | 213 | it('should get the tip', async () => { 214 | const tip = await rpcs.getTip({ currency }); 215 | assert(tip != undefined); 216 | expect(tip).to.have.property('hash'); 217 | expect(tip).to.have.property('height'); 218 | }); 219 | 220 | it('should validate address', async () => { 221 | const isValid = await rpcs.validateAddress({ currency, address: config.currencyConfig.sendTo }); 222 | assert(isValid === true); 223 | }); 224 | 225 | it('should not validate bad address', async () => { 226 | const isValid = await rpcs.validateAddress({ currency, address: 'NOTANADDRESS' }); 227 | assert(isValid === false); 228 | }); 229 | 230 | it('should be able to send a batched transaction', async() => { 231 | let address1 = 'bchreg:qq2lqjaeut5ppjkx9339htfed8enx7hmugk37ytwqy'; 232 | let amount1 = 10000; 233 | let address2 = 'bchreg:qq6n0n37mut4353m9k2zm5nh0pejk7vh7u77tan544'; 234 | let amount2 = 20000; 235 | const batch = {}; 236 | batch[address1] = amount1; 237 | batch[address2] = amount2; 238 | 239 | await bitcoin.walletUnlock({ passphrase: config.currencyConfig.unlockPassword, time: 10 }); 240 | let txid = await bitcoin.sendMany({ batch, options: null }); 241 | await bitcoin.walletLock(); 242 | expect(txid).to.have.lengthOf(64); 243 | assert(txid); 244 | }); 245 | 246 | it('should be able to get server info', async () => { 247 | const info = await rpcs.getServerInfo({ currency }); 248 | expect(info).to.have.property('chain'); 249 | expect(info).to.have.property('blocks'); 250 | expect(info).to.have.property('headers'); 251 | expect(info).to.have.property('bestblockhash'); 252 | expect(info).to.have.property('difficulty'); 253 | // expect(info).to.have.property('time'); 254 | expect(info).to.have.property('mediantime'); 255 | expect(info).to.have.property('verificationprogress'); 256 | expect(info).to.have.property('initialblockdownload'); 257 | expect(info).to.have.property('chainwork'); 258 | expect(info).to.have.property('size_on_disk'); 259 | expect(info).to.have.property('pruned'); 260 | expect(info).to.have.property('warnings'); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /tests/btc.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const assert = require('assert'); 3 | const mocha = require('mocha'); 4 | const sinon = require('sinon'); 5 | const { expect } = require('chai'); 6 | const { describe, it } = mocha; 7 | const config = { 8 | chain: 'BTC', 9 | host: process.env.HOST_BTC || 'bitcoin', 10 | protocol: 'http', 11 | rpcPort: '8333', 12 | rpcUser: 'cryptorpc', 13 | rpcPass: 'local321', 14 | tokens: {}, 15 | currencyConfig: { 16 | sendTo: '2NGFWyW3LBPr6StDuDSNFzQF3Jouuup1rua', 17 | unlockPassword: 'password', 18 | rawTx: 19 | '0100000001641ba2d21efa8db1a08c0072663adf4c4bc3be9ee5aabb530b2d4080b8a41cca000000006a4730440220062105df71eb10b5ead104826e388303a59d5d3d134af73cdf0d5e685650f95c0220188c8a966a2d586430d84aa7624152a556550c3243baad5415c92767dcad257f0121037aaa54736c5ffa13132e8ca821be16ce4034ae79472053dde5aa4347034bc0a2ffffffff0240787d010000000017a914c8241f574dfade4d446ec90cc0e534cb120b45e387eada4f1c000000001976a9141576306b9cc227279b2a6c95c2b017bb22b0421f88ac00000000' 20 | } 21 | }; 22 | 23 | describe('BTC Tests', function() { 24 | this.timeout(10000); 25 | const currency = 'BTC'; 26 | const { currencyConfig } = config; 27 | const rpcs = new CryptoRpc(config, currencyConfig); 28 | const bitcoin = rpcs.get(currency); 29 | const walletName = 'wallet0'; 30 | const addressLabel = 'abc123'; 31 | let walletAddress = ''; 32 | 33 | before(async () => { 34 | await bitcoin.asyncCall('createwallet', [walletName]); 35 | walletAddress = await bitcoin.asyncCall('getnewaddress', [addressLabel]); 36 | }); 37 | 38 | it('should determine if wallet is encrypted', async () => { 39 | expect(await bitcoin.isWalletEncrypted()).to.eq(false); 40 | try { 41 | await bitcoin.asyncCall('encryptWallet', ['password']); 42 | await new Promise(resolve => setTimeout(resolve, 5000)); 43 | } catch (e) { 44 | console.warn('wallet already encrypted'); 45 | } 46 | expect(await bitcoin.isWalletEncrypted()).to.eq(true); 47 | await bitcoin.asyncCall('generatetoaddress', [101, walletAddress]); 48 | }); 49 | 50 | it('walletUnlock should unlock wallet successfully', async () => { 51 | await bitcoin.walletUnlock({ passphrase: config.currencyConfig.unlockPassword, time: 10 }); 52 | }); 53 | 54 | it('walletUnlock should error on if wrong args', async () => { 55 | await bitcoin.walletUnlock({ passphrase: config.currencyConfig.unlockPassword }) 56 | .catch(err => { 57 | assert(err); 58 | expect(typeof err).to.eq('object'); 59 | expect(err).to.have.property('message'); 60 | expect(err.message).to.eq('JSON value is not an integer as expected'); 61 | }); 62 | }); 63 | 64 | it('walletUnlock should error on if wrong passphrase', async () => { 65 | await bitcoin.walletUnlock({ passphrase: 'wrong', time: 10 }) 66 | .catch(err => { 67 | assert(err); 68 | expect(typeof err).to.eq('object'); 69 | expect(err).to.have.property('message'); 70 | expect(err.message).to.eq('Error: The wallet passphrase entered was incorrect.'); 71 | }); 72 | }); 73 | 74 | it('should be able to get a block hash', async () => { 75 | const blockHash = await rpcs.getBestBlockHash({ currency }); 76 | expect(blockHash).to.have.lengthOf('64'); 77 | }); 78 | 79 | /* These tests don't work in the pipeline because the docker regtest blockchain isn't mature enough to give a fee */ 80 | // it('should be able to estimateFee', async () => { 81 | // const fee = await bitcoin.estimateFee({ nBlocks: 2 }); 82 | // expect(fee).to.be.gte(1); 83 | // }); 84 | 85 | // it('should be able to estimateFee with mode', async () => { 86 | // const fee = await bitcoin.estimateFee({ nBlocks: 2, mode: 'economical' }); 87 | // expect(fee).to.be.gte(1); 88 | // }); 89 | 90 | it('should convert fee to satoshis per kilobyte with estimateFee', async () => { 91 | sinon.stub(bitcoin.rpc, 'estimateSmartFee').callsFake((nBlocks, cb) => { 92 | cb(null, { result: { feerate: 0.00001234, blocks: 2 } }); 93 | }); 94 | const fee = await bitcoin.estimateFee({ nBlocks: 2 }); 95 | expect(fee).to.be.eq(1.234); 96 | }); 97 | 98 | it('should not estimateMaxPriorityFee for non EVM chain', async () => { 99 | const fee = await rpcs.estimateMaxPriorityFee({ currency, nBlocks: 2 }); 100 | expect(fee).to.be.eq(undefined); 101 | }); 102 | 103 | it('should be able to get a balance', async () => { 104 | const balance = await rpcs.getBalance({ currency }); 105 | expect(balance).to.eq(5000000000); 106 | assert(balance != undefined); 107 | }); 108 | 109 | it('should be able to send a transaction', async () => { 110 | const txid = await rpcs.unlockAndSendToAddress({ currency, address: walletAddress, amount: '10000', passphrase: currencyConfig.unlockPassword }); 111 | expect(txid).to.have.lengthOf(64); 112 | }); 113 | 114 | it('should be able to send many transactions', async () => { 115 | let payToArray = []; 116 | const transaction1 = { 117 | address: 'mm7mGjBBe1sUF8SFXCW779DX8XrmpReBTg', 118 | amount: 10000 119 | }; 120 | const transaction2 = { 121 | address: 'mm7mGjBBe1sUF8SFXCW779DX8XrmpReBTg', 122 | amount: 20000 123 | }; 124 | const transaction3 = { 125 | address: 'mgoVRuvgbgyZL8iQWfS6TLPZzQnpRMHg5H', 126 | amount: 30000 127 | }; 128 | const transaction4 = { 129 | address: 'mv5XmsNbK2deMDhkVq5M28BAD14hvpQ9b2', 130 | amount: 40000 131 | }; 132 | payToArray.push(transaction1); 133 | payToArray.push(transaction2); 134 | payToArray.push(transaction3); 135 | payToArray.push(transaction4); 136 | const maxOutputs = 2; 137 | const maxValue = 1e8; 138 | const eventEmitter = rpcs.rpcs.BTC.emitter; 139 | let eventCounter = 0; 140 | let emitResults = []; 141 | const emitPromise = new Promise(resolve => { 142 | eventEmitter.on('success', (emitData) => { 143 | eventCounter++; 144 | emitResults.push(emitData); 145 | if (eventCounter === 3) { 146 | resolve(); 147 | } 148 | }); 149 | }); 150 | const outputArray = await rpcs.unlockAndSendToAddressMany({ payToArray, passphrase: currencyConfig.unlockPassword, time: 1000, maxValue, maxOutputs }); 151 | await emitPromise; 152 | expect(outputArray).to.have.lengthOf(4); 153 | expect(outputArray[0].txid).to.equal(outputArray[1].txid); 154 | expect(outputArray[2].txid).to.equal(outputArray[3].txid); 155 | expect(outputArray[1].txid).to.not.equal(outputArray[2].txid); 156 | for (let transaction of outputArray) { 157 | assert(transaction.txid); 158 | expect(transaction.txid).to.have.lengthOf(64); 159 | } 160 | for (let emitData of emitResults) { 161 | assert(emitData.address); 162 | assert(emitData.amount); 163 | assert(emitData.txid); 164 | expect(emitData.error === null); 165 | expect(emitData.vout === 0 || emitData.vout === 1); 166 | let transactionObj = {address: emitData.address, amount: emitData.amount}; 167 | expect(payToArray.includes(transactionObj)); 168 | } 169 | }); 170 | 171 | it('should reject when one of many transactions fails', async () => { 172 | const address = config.currencyConfig.sendTo; 173 | const amount = '1000'; 174 | const payToArray = [ 175 | { address, amount }, 176 | { address: 'funkyColdMedina', amount: 1 } 177 | ]; 178 | const eventEmitter = rpcs.rpcs.BTC.emitter; 179 | let emitResults = []; 180 | const emitPromise = new Promise(resolve => { 181 | eventEmitter.on('failure', (emitData) => { 182 | emitResults.push(emitData); 183 | resolve(); 184 | }); 185 | }); 186 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 187 | currency, 188 | payToArray, 189 | passphrase: currencyConfig.unlockPassword 190 | }); 191 | await emitPromise; 192 | assert(!outputArray[1].txid); 193 | expect(outputArray[1].error).to.equal(emitResults[0].error); 194 | expect(emitResults.length).to.equal(1); 195 | assert(emitResults[0].error); 196 | }); 197 | 198 | it('should be able to decode a raw transaction', async () => { 199 | const { rawTx } = config.currencyConfig; 200 | assert(rawTx); 201 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 202 | expect(decoded).to.have.property('txid'); 203 | expect(decoded).to.have.property('hash'); 204 | expect(decoded).to.have.property('version'); 205 | expect(decoded).to.have.property('size'); 206 | expect(decoded).to.have.property('vsize'); 207 | expect(decoded).to.have.property('locktime'); 208 | expect(decoded).to.have.property('vin'); 209 | expect(decoded).to.have.property('vout'); 210 | assert(decoded); 211 | }); 212 | 213 | it('should get the tip', async () => { 214 | const tip = await rpcs.getTip({ currency }); 215 | assert(tip != undefined); 216 | expect(tip).to.have.property('hash'); 217 | expect(tip).to.have.property('height'); 218 | }); 219 | 220 | it('should validate address', async () => { 221 | const isValid = await rpcs.validateAddress({ currency, address: config.currencyConfig.sendTo }); 222 | assert(isValid === true); 223 | }); 224 | 225 | it('should not validate bad address', async () => { 226 | const isValid = await rpcs.validateAddress({ currency, address: 'NOTANADDRESS' }); 227 | assert(isValid === false); 228 | }); 229 | 230 | it('should be able to send a batched transaction', async() => { 231 | let address1 = 'mtXWDB6k5yC5v7TcwKZHB89SUp85yCKshy'; 232 | let amount1 = '10000'; 233 | let address2 = 'msngvArStqsSqmkG7W7Fc9jotPcURyLyYu'; 234 | let amount2 = '20000'; 235 | const batch = {}; 236 | batch[address1] = amount1; 237 | batch[address2] = amount2; 238 | 239 | await bitcoin.walletUnlock({ passphrase: config.currencyConfig.unlockPassword, time: 10 }); 240 | let txid = await bitcoin.sendMany({ batch, options: null }); 241 | await bitcoin.walletLock(); 242 | expect(txid).to.have.lengthOf(64); 243 | assert(txid); 244 | }); 245 | 246 | describe('Get Blocks', function() { 247 | let blockHash; 248 | before(async () => { 249 | blockHash = await rpcs.getBestBlockHash({ currency }); 250 | expect(blockHash).to.have.lengthOf('64'); 251 | }); 252 | 253 | it('should get block', async () => { 254 | const reqBlock = await rpcs.getBlock({ currency, hash: blockHash }); 255 | expect(reqBlock).to.have.property('hash'); 256 | expect(reqBlock).to.have.property('confirmations'); 257 | expect(reqBlock).to.have.property('strippedsize'); 258 | expect(reqBlock).to.have.property('size'); 259 | expect(reqBlock).to.have.property('weight'); 260 | expect(reqBlock).to.have.property('height'); 261 | expect(reqBlock).to.have.property('version'); 262 | expect(reqBlock).to.have.property('versionHex'); 263 | expect(reqBlock).to.have.property('merkleroot'); 264 | expect(reqBlock).to.have.property('tx'); 265 | expect(reqBlock).to.have.property('time'); 266 | expect(reqBlock).to.have.property('mediantime'); 267 | expect(reqBlock).to.have.property('nonce'); 268 | expect(reqBlock).to.have.property('bits'); 269 | expect(reqBlock).to.have.property('difficulty'); 270 | expect(reqBlock).to.have.property('chainwork'); 271 | expect(reqBlock).to.have.property('nTx'); 272 | expect(reqBlock).to.have.property('previousblockhash'); 273 | assert(reqBlock); 274 | }); 275 | 276 | }); 277 | 278 | describe('Get Transactions', function() { 279 | let txid; 280 | before(async () => { 281 | txid = await rpcs.unlockAndSendToAddress({ currency, address: walletAddress, amount: '10000', passphrase: currencyConfig.unlockPassword }); 282 | expect(txid).to.have.lengthOf(64); 283 | assert(txid); 284 | }); 285 | 286 | it('should be able to get a transaction', async () => { 287 | const tx = await rpcs.getTransaction({ currency, txid }); 288 | expect(tx).to.have.property('txid'); 289 | expect(tx).to.have.property('hash'); 290 | expect(tx).to.have.property('version'); 291 | expect(tx).to.have.property('size'); 292 | expect(tx).to.have.property('vsize'); 293 | expect(tx).to.have.property('locktime'); 294 | expect(tx).to.have.property('vin'); 295 | expect(tx).to.have.property('vout'); 296 | expect(tx).to.have.property('hex'); 297 | assert(tx); 298 | assert(typeof tx === 'object'); 299 | }); 300 | 301 | it('should be able to get a transaction with detail', async() => { 302 | const tx = await rpcs.getTransaction({ txid, detail: true }); 303 | expect(tx).to.exist; 304 | expect(tx.txid).to.equal(txid); 305 | expect(tx.vin[0].address).to.exist; 306 | expect(tx.vin[0].value).to.exist; 307 | }); 308 | 309 | it('should get confirmations', async () => { 310 | let confirmations = await rpcs.getConfirmations({ currency, txid }); 311 | assert(confirmations != undefined); 312 | expect(confirmations).to.eq(0); 313 | await bitcoin.asyncCall('generatetoaddress', [1, config.currencyConfig.sendTo]); 314 | confirmations = await rpcs.getConfirmations({ currency, txid }); 315 | expect(confirmations).to.eq(1); 316 | }); 317 | }); 318 | 319 | describe('Get Wallet Transactions', function() { 320 | let txs; 321 | 322 | describe('Unloaded wallet', function() { 323 | before(async () => { 324 | await bitcoin.asyncCall('unloadwallet', [walletName]); 325 | }); 326 | after(async () => { 327 | await bitcoin.asyncCall('loadwallet', [walletName]); 328 | }); 329 | 330 | it('should error if no wallet is loaded', async () => { 331 | try { 332 | await rpcs.getTransactions({ currency }); 333 | throw new Error('should have thrown'); 334 | } catch (e) { 335 | expect(e.message).to.include('No wallet is loaded'); 336 | } 337 | }); 338 | }); 339 | 340 | it('should return wallet transactions', async () => { 341 | txs = await rpcs.getTransactions({ currency }); 342 | expect(txs).to.have.lengthOf(10); 343 | }); 344 | 345 | it('should return wallet transactions with count and skip', async () => { 346 | const _txs = await rpcs.getTransactions({ currency, count: 2, skip: 3 }); 347 | expect(_txs).to.have.lengthOf(2); 348 | // txs are ordered in ascending order, so latest are at the bottom. 349 | expect(_txs[1].txid).to.equal(txs.slice(-3)[0].txid); 350 | }); 351 | 352 | it('should return wallet transactions by label', async () => { 353 | const _txs = await rpcs.getTransactions({ currency, label: addressLabel }); 354 | expect(_txs).to.have.lengthOf(10); 355 | expect(_txs.every(t => t.label === addressLabel)).to.equal(true); 356 | expect(_txs[0].txid).to.not.equal(txs[0].txid); // makes sure it's not just returning the same array 357 | }); 358 | 359 | }); 360 | 361 | describe('Tx outputs', function() { 362 | let txid; 363 | before(async () => { 364 | txid = await rpcs.unlockAndSendToAddress({ currency, address: config.currencyConfig.sendTo, amount: '10000', passphrase: currencyConfig.unlockPassword }); 365 | expect(txid).to.have.lengthOf(64); 366 | assert(txid); 367 | }); 368 | it('should get tx output info from mempool', async() => { 369 | const output1 = await rpcs.getTxOutputInfo({ txid, vout: 0, includeMempool: true }); 370 | const output2 = await rpcs.getTxOutputInfo({ txid, vout: 1, includeMempool: true }); 371 | let output = [output1, output2].find(v => v.value === 0.0001); 372 | expect(output).to.exist; 373 | expect(output.scriptPubKey.address).to.equal(config.currencyConfig.sendTo); 374 | }); 375 | 376 | it('should fail to get tx output when not in mempool', async() => { 377 | let output = null; 378 | try { 379 | output = await rpcs.getTxOutputInfo({ txid, vout: 0, includeMempool: false }); 380 | } catch (e) { 381 | expect(e.message).to.include('No info found for'); 382 | } 383 | expect(output).to.be.null; 384 | }); 385 | 386 | describe('Tx output after confirmation',function() { 387 | before(async () => { 388 | let confirmations = await rpcs.getConfirmations({ currency, txid }); 389 | assert(confirmations != undefined); 390 | expect(confirmations).to.eq(0); 391 | await bitcoin.asyncCall('generatetoaddress', [1, config.currencyConfig.sendTo]); 392 | confirmations = await rpcs.getConfirmations({ currency, txid }); 393 | expect(confirmations).to.eq(1); 394 | }); 395 | 396 | it('should get tx output info', async() => { 397 | const output1 = await rpcs.getTxOutputInfo({ txid, vout: 0 }); 398 | const output2 = await rpcs.getTxOutputInfo({ txid, vout: 1 }); 399 | let output = [output1, output2].find(v => v.value === 0.0001); 400 | expect(output).to.exist; 401 | expect(output.scriptPubKey.address).to.equal(config.currencyConfig.sendTo); 402 | }); 403 | 404 | it('should get tx output info for bitcore', async() => { 405 | const output1 = await rpcs.getTxOutputInfo({ txid, vout: 0, transformToBitcore: true }); 406 | const output2 = await rpcs.getTxOutputInfo({ txid, vout: 1, transformToBitcore: true }); 407 | let output = [output1, output2].find(v => v.value === 0.0001); 408 | expect(output).to.exist; 409 | expect(output.address).to.equal(config.currencyConfig.sendTo); 410 | expect(output.mintTxid).to.equal(txid); 411 | }); 412 | }); 413 | }); 414 | 415 | it('should be able to get server info', async () => { 416 | const info = await rpcs.getServerInfo({ currency }); 417 | expect(info).to.have.property('chain'); 418 | expect(info).to.have.property('blocks'); 419 | expect(info).to.have.property('headers'); 420 | expect(info).to.have.property('bestblockhash'); 421 | expect(info).to.have.property('difficulty'); 422 | // expect(info).to.have.property('time'); // TODO this is added in newer bitcoin core version 423 | expect(info).to.have.property('mediantime'); 424 | expect(info).to.have.property('verificationprogress'); 425 | expect(info).to.have.property('initialblockdownload'); 426 | expect(info).to.have.property('chainwork'); 427 | // expect(info).to.have.property('size_on_disk'); // TODO this is added in newer bitcoin core version 428 | expect(info).to.have.property('pruned'); 429 | expect(info).to.have.property('warnings'); 430 | }); 431 | }); 432 | -------------------------------------------------------------------------------- /tests/docker/Dockerfile-test: -------------------------------------------------------------------------------- 1 | FROM node:22 2 | WORKDIR /crypto-rpc 3 | COPY package.json . 4 | RUN mkdir ~/.ssh 5 | RUN ssh-keyscan github.com >> ~/.ssh/known_hosts 6 | RUN npm install 7 | ADD . . 8 | ENV PATH="/crypto-rpc/tests/docker/solc-v0.4.24:${PATH}" 9 | CMD ["npm", "run", "docker:test"] 10 | -------------------------------------------------------------------------------- /tests/docker/ganache.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | RUN npm install -g ganache-cli 3 | ENTRYPOINT ["ganache-cli", "-i", "1337", "-m", "kiss talent nerve fossil equip fault exile execute train wrist misery diet","-h", "0.0.0.0"] 4 | -------------------------------------------------------------------------------- /tests/docker/geth-keystore/UTC--2022-09-02T14-12-02.445263618Z--00a329c0648769a73afac7f9381e08fb43dbea72: -------------------------------------------------------------------------------- 1 | {"address":"00a329c0648769a73afac7f9381e08fb43dbea72","crypto":{"cipher":"aes-128-ctr","ciphertext":"3e794ac9cb059fe12c891b11157a08d0c5c0283561ad39bd7be9561a88386876","cipherparams":{"iv":"200f81acf66c35f3923c419022eede47"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"998b56f65d550407d18d856bb4e9e361cdb48ed11be40e8b2783303fb5a97c3b"},"mac":"d311f08a306a07687aa8c234182d110defbff1cc96be54ed1b6165b2d4fbb26d"},"id":"11155206-b093-4f4d-86be-338e22cc88bc","version":3} -------------------------------------------------------------------------------- /tests/docker/geth-keystore/pw: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/docker/rippled.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-bullseye 2 | 3 | RUN apt-get update 4 | RUN apt-get install sudo 5 | RUN adduser --disabled-password --gecos '' docker 6 | RUN adduser docker sudo 7 | RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 8 | USER docker 9 | 10 | RUN sudo apt -y update 11 | RUN sudo apt -y install apt-transport-https ca-certificates wget gnupg 12 | RUN wget -q -O - "https://repos.ripple.com/repos/api/gpg/key/public" | sudo apt-key add - 13 | RUN echo "deb https://repos.ripple.com/repos/rippled-deb bullseye stable" | sudo tee -a /etc/apt/sources.list.d/ripple.list 14 | RUN sudo apt -y update 15 | RUN sudo apt -y install rippled 16 | 17 | RUN sudo rm /etc/opt/ripple/rippled.cfg 18 | COPY tests/docker/rippled.cfg /home/docker 19 | RUN sudo cp /home/docker/rippled.cfg /etc/opt/ripple/rippled.cfg 20 | 21 | ENTRYPOINT ["sudo", "rippled", "-a", "--start", "--conf=/home/docker/rippled.cfg"] 22 | EXPOSE 6006 23 | EXPOSE 6005 24 | EXPOSE 5005 25 | EXPOSE 5004 26 | -------------------------------------------------------------------------------- /tests/docker/rippled.cfg: -------------------------------------------------------------------------------- 1 | [server] 2 | port_rpc_admin_local 3 | port_peer 4 | port_ws_admin_local 5 | 6 | [port_rpc_admin_local] 7 | port = 5005 8 | ip = 0.0.0.0 9 | admin = 0.0.0.0 10 | protocol = http 11 | 12 | [port_peer] 13 | port = 51235 14 | ip = 0.0.0.0 15 | protocol = peer 16 | 17 | [port_ws_admin_local] 18 | port = 6006 19 | ip = 0.0.0.0 20 | admin = 0.0.0.0 21 | protocol = ws 22 | 23 | #------------------------------------------------------------------------------- 24 | 25 | [node_size] 26 | medium 27 | 28 | [node_db] 29 | type=RocksDB 30 | path=/var/lib/rippled/regtest/db/rocksdb 31 | open_files=2000 32 | filter_bits=12 33 | cache_mb=256 34 | file_size_mb=8 35 | file_size_mult=2 36 | online_delete=2000 37 | advisory_delete=0 38 | 39 | [database_path] 40 | /var/lib/rippled/regtest/db 41 | 42 | [debug_logfile] 43 | /var/lib/rippled/regtest/debug.log 44 | 45 | [sntp_servers] 46 | time.windows.com 47 | time.apple.com 48 | time.nist.gov 49 | pool.ntp.org 50 | 51 | [rpc_startup] 52 | { "command": "log_level", "severity": "warning" } 53 | 54 | [ssl_verify] 55 | 1 56 | -------------------------------------------------------------------------------- /tests/docker/solana.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-bullseye 2 | WORKDIR /crypto-rpc 3 | RUN npm install @solana/kit 4 | 5 | # Use the official Solana image 6 | FROM solanalabs/solana:v1.18.26 7 | 8 | # Copy keypair files to the container 9 | COPY ./blockchain/solana/test/keypair/id.json /solana/keypair/id.json 10 | COPY ./blockchain/solana/test/keypair/id2.json /solana/keypair/id2.json 11 | COPY ./blockchain/solana/test/keypair/id3.json /solana/keypair/id3.json 12 | COPY ./blockchain/solana/test/keypair/validator.json /root/.config/solana/id.json 13 | 14 | # Add a script to start the validator and fund the addresses 15 | COPY ./blockchain/solana/test/startSolana.sh /solana/startSolana.sh 16 | 17 | # Make the script executable 18 | RUN chmod +x /solana/startSolana.sh 19 | 20 | ENTRYPOINT ["./solana/startSolana.sh"] 21 | EXPOSE 8899 22 | EXPOSE 8900 -------------------------------------------------------------------------------- /tests/docker/solc-v0.4.24/solc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitpay/crypto-rpc/d823f70c13937e332289f75728c52bf7d3c81de5/tests/docker/solc-v0.4.24/solc -------------------------------------------------------------------------------- /tests/doge.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const sinon = require('sinon'); 3 | const assert = require('assert'); 4 | const mocha = require('mocha'); 5 | const { expect } = require('chai'); 6 | const { before, describe, it } = mocha; 7 | const config = { 8 | chain: 'DOGE', 9 | host: process.env.HOST_DOGE || 'dogecoin', 10 | protocol: 'http', 11 | rpcPort: '22555', 12 | rpcUser: 'cryptorpc', 13 | rpcPass: 'local321', 14 | tokens: {}, 15 | currencyConfig: { 16 | sendTo: 'n35ckY9BRmmjs9CCFfkaZAjeDpdaY4phRZ', 17 | unlockPassword: 'password', 18 | rawTx: 19 | '01000000018e767be30f1e4a70dcaf2b2374fe26cfbd624d7cb3e6b17244b7100abcf4dbad0000000049483045022100fe9578607f05acd8484dff649afb8efbd3fcf3bce72ac4b1a345a1a22849f805022078a29ba99250a25a8000fd76675167088c2798626c480ac2c6b41fcb4868163601feffffff02004d4158782d00001976a914920f410c5799da55aad17ebf4d360eecf0ba481088ac00f2052a010000001976a914ec880de03abdb41d875ad5290ad59bbf5653fcd488ac53000000' 20 | } 21 | }; 22 | 23 | describe('DOGE Tests', function() { 24 | this.timeout(20000); 25 | let blockHash = ''; 26 | const currency = 'DOGE'; 27 | const { currencyConfig } = config; 28 | const rpcs = new CryptoRpc(config, currencyConfig); 29 | const bitcoin = rpcs.get(currency); 30 | 31 | before(async () => { 32 | try { 33 | await bitcoin.asyncCall('encryptWallet', ['password']); 34 | } catch (e) { 35 | console.warn('wallet already encrypted'); 36 | } 37 | await new Promise(resolve => setTimeout(resolve, 5000)); 38 | await bitcoin.asyncCall('generate', [101]); 39 | }); 40 | 41 | 42 | it('should be able to get a block hash', async () => { 43 | blockHash = await rpcs.getBestBlockHash({ currency }); 44 | expect(blockHash).to.have.lengthOf('64'); 45 | }); 46 | 47 | 48 | it('should convert fee to satoshis per kilobyte with estimateFee', async () => { 49 | sinon.stub(bitcoin.rpc,'estimateSmartFee').callsFake((nBlocks, cb) => { 50 | cb(null, {result: {'feerate': 0.00001234, 'blocks': 2}}); 51 | }); 52 | const fee = await bitcoin.estimateFee({nBlocks: 2}); 53 | expect(fee).to.be.eq(1.234); 54 | }); 55 | 56 | it('should get block', async () => { 57 | const reqBlock = await rpcs.getBlock({ currency, hash: blockHash }); 58 | expect(reqBlock).to.have.property('hash'); 59 | expect(reqBlock).to.have.property('confirmations'); 60 | expect(reqBlock).to.have.property('size'); 61 | expect(reqBlock).to.have.property('height'); 62 | expect(reqBlock).to.have.property('version'); 63 | expect(reqBlock).to.have.property('versionHex'); 64 | expect(reqBlock).to.have.property('merkleroot'); 65 | expect(reqBlock).to.have.property('tx'); 66 | expect(reqBlock).to.have.property('time'); 67 | expect(reqBlock).to.have.property('mediantime'); 68 | expect(reqBlock).to.have.property('nonce'); 69 | expect(reqBlock).to.have.property('bits'); 70 | expect(reqBlock).to.have.property('difficulty'); 71 | expect(reqBlock).to.have.property('chainwork'); 72 | expect(reqBlock).to.have.property('previousblockhash'); 73 | assert(reqBlock); 74 | }); 75 | 76 | it('should be able to get a balance', async () => { 77 | const balance = await rpcs.getBalance({ currency }); 78 | expect(balance).to.eq(2050000000000000); 79 | assert(balance != undefined); 80 | }); 81 | 82 | it('should be able to send a transaction', async () => { 83 | const txid = await rpcs.unlockAndSendToAddress({ currency, address: config.currencyConfig.sendTo, amount: '10000', passphrase: currencyConfig.unlockPassword }); 84 | expect(txid).to.have.lengthOf(64); 85 | assert(txid); 86 | await bitcoin.asyncCall('generate', [2]); 87 | 88 | it('should get confirmations', async () => { 89 | let confirmations = await rpcs.getConfirmations({ currency, txid }); 90 | assert(confirmations != undefined); 91 | expect(confirmations).to.eq(2); 92 | }); 93 | 94 | 95 | it('should be able to get a transaction', async () => { 96 | const tx = await rpcs.getTransaction({ currency, txid }); 97 | expect(tx).to.have.property('txid'); 98 | expect(tx).to.have.property('hash'); 99 | expect(tx).to.have.property('version'); 100 | expect(tx).to.have.property('size'); 101 | expect(tx).to.have.property('locktime'); 102 | expect(tx).to.have.property('vin'); 103 | expect(tx).to.have.property('vout'); 104 | expect(tx).to.have.property('hex'); 105 | assert(tx); 106 | assert(typeof tx === 'object'); 107 | }); 108 | }); 109 | 110 | it('should be able to send many transactions', async () => { 111 | let payToArray = []; 112 | let transaction1 = { 113 | address: 'mzVk8WuGZGvFP6qfrbeAyu7oFFRFaMg5oB', 114 | amount: 10000 115 | }; 116 | let transaction2 = { 117 | address: 'mpqVKMU5iGz9oaGKptgRXhAyyR2WyZFh3a', 118 | amount: 20000 119 | }; 120 | let transaction3 = { 121 | address: 'mnhQ2e7mqsat8wfuhiE96z6JcZDnSBFz3F', 122 | amount: 30000 123 | }; 124 | let transaction4 = { 125 | address: 'mshywUvMRg1oNcAefEaL5UVDqe6NjuoHid', 126 | amount: 40000 127 | }; 128 | payToArray.push(transaction1); 129 | payToArray.push(transaction2); 130 | payToArray.push(transaction3); 131 | payToArray.push(transaction4); 132 | const maxOutputs = 2; 133 | const maxValue = 1e8; 134 | const eventEmitter = rpcs.rpcs.DOGE.emitter; 135 | let eventCounter = 0; 136 | let emitResults = []; 137 | const emitPromise = new Promise(resolve => { 138 | eventEmitter.on('success', (emitData) => { 139 | eventCounter++; 140 | emitResults.push(emitData); 141 | if (eventCounter === 3) { 142 | resolve(); 143 | } 144 | }); 145 | }); 146 | const outputArray = await rpcs.unlockAndSendToAddressMany({ payToArray, passphrase: currencyConfig.unlockPassword, time: 1000, maxValue, maxOutputs }); 147 | await emitPromise; 148 | expect(outputArray).to.have.lengthOf(4); 149 | expect(outputArray[0].txid).to.equal(outputArray[1].txid); 150 | expect(outputArray[2].txid).to.equal(outputArray[3].txid); 151 | expect(outputArray[1].txid).to.not.equal(outputArray[2].txid); 152 | for (let transaction of outputArray) { 153 | assert(transaction.txid); 154 | expect(transaction.txid).to.have.lengthOf(64); 155 | } 156 | for (let emitData of emitResults) { 157 | assert(emitData.address); 158 | assert(emitData.amount); 159 | assert(emitData.txid); 160 | expect(emitData.error === null); 161 | expect(emitData.vout === 0 || emitData.vout === 1); 162 | let transactionObj = {address: emitData.address, amount: emitData.amount}; 163 | expect(payToArray.includes(transactionObj)); 164 | } 165 | 166 | await bitcoin.asyncCall('generate', [10]); 167 | }); 168 | 169 | it('should reject when one of many transactions fails', async () => { 170 | const payToArray = [ 171 | { address: 'mshywUvMRg1oNcAefEaL5UVDqe6NjuoHid', 172 | amount: 10000 173 | }, 174 | { address: 'funkyColdMedina', 175 | amount: 1 176 | }, 177 | ]; 178 | const eventEmitter = rpcs.rpcs.DOGE.emitter; 179 | let emitResults = []; 180 | const emitPromise = new Promise(resolve => { 181 | eventEmitter.on('failure', (emitData) => { 182 | emitResults.push(emitData); 183 | resolve(); 184 | }); 185 | }); 186 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 187 | currency, 188 | payToArray, 189 | passphrase: currencyConfig.unlockPassword 190 | }); 191 | await emitPromise; 192 | assert(!outputArray[1].txid); 193 | expect(outputArray[1].error).to.equal(emitResults[0].error); 194 | expect(emitResults.length).to.equal(1); 195 | assert(emitResults[0].error); 196 | }); 197 | 198 | 199 | it('should be able to decode a raw transaction', async () => { 200 | const { rawTx } = config.currencyConfig; 201 | assert(rawTx); 202 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 203 | expect(decoded).to.have.property('txid'); 204 | expect(decoded).to.have.property('hash'); 205 | expect(decoded).to.have.property('version'); 206 | expect(decoded).to.have.property('size'); 207 | expect(decoded).to.have.property('locktime'); 208 | expect(decoded).to.have.property('vin'); 209 | expect(decoded).to.have.property('vout'); 210 | assert(decoded); 211 | }); 212 | 213 | it('should get the tip', async () => { 214 | const tip = await rpcs.getTip({ currency }); 215 | assert(tip != undefined); 216 | expect(tip).to.have.property('hash'); 217 | expect(tip).to.have.property('height'); 218 | }); 219 | 220 | it('should validate address', async () => { 221 | const isValid = await rpcs.validateAddress({ currency, address: config.currencyConfig.sendTo }); 222 | assert(isValid === true); 223 | }); 224 | 225 | it('should not validate bad address', async () => { 226 | const isValid = await rpcs.validateAddress({ currency, address: 'NOTANADDRESS' }); 227 | assert(isValid === false); 228 | }); 229 | 230 | it('should be able to send a batched transaction', async() => { 231 | let address1 = 'mrc9MauBwqPGkLP7wckjfPX2Y8ZpWXnGLD'; 232 | let amount1 = 10000; 233 | let address2 = 'n3NB6EWrMWvGobFL2JE5tThiiM5Eh3yWvT'; 234 | let amount2 = 20000; 235 | const batch = {}; 236 | batch[address1] = amount1; 237 | batch[address2] = amount2; 238 | 239 | await bitcoin.walletUnlock({ passphrase: config.currencyConfig.unlockPassword, time: 10 }); 240 | let txid = await bitcoin.sendMany({ batch, options: null }); 241 | await bitcoin.walletLock(); 242 | expect(txid).to.have.lengthOf(64); 243 | assert(txid); 244 | }); 245 | 246 | it('should be able to get server info', async () => { 247 | const info = await rpcs.getServerInfo({ currency }); 248 | expect(info).to.have.property('chain'); 249 | expect(info).to.have.property('blocks'); 250 | expect(info).to.have.property('headers'); 251 | expect(info).to.have.property('bestblockhash'); 252 | expect(info).to.have.property('difficulty'); 253 | expect(info).to.have.property('mediantime'); 254 | expect(info).to.have.property('verificationprogress'); 255 | expect(info).to.have.property('initialblockdownload'); 256 | expect(info).to.have.property('chainwork'); 257 | expect(info).to.have.property('pruned'); 258 | expect(info).to.have.property('softforks'); 259 | expect(info).to.have.property('bip9_softforks'); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /tests/erc20.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const {assert, expect} = require('chai'); 3 | const mocha = require('mocha'); 4 | const util = require('web3-utils'); 5 | const { before, describe, it } = mocha; 6 | const ERC20 = require('../blockchain/EVM/build/contracts/CryptoErc20.json'); 7 | const config = { 8 | chain: 'ETH', 9 | host: process.env.HOST_ETH || 'geth', 10 | protocol: 'http', 11 | rpcPort: '8545', 12 | account: '0x00a329c0648769A73afAc7F9381E08FB43dBEA72', 13 | tokens: { 14 | ERC20: { 15 | tokenContractAddress: ERC20.networks['1337'].address, 16 | type: 'ERC20' 17 | } 18 | }, 19 | currencyConfig: { 20 | sendTo: '0xA15035277A973d584b1d6150e93C21152D6Af440', 21 | unlockPassword: '', 22 | privateKey: 23 | '0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7', 24 | rawTx: 25 | '0xf8978202e38471a14e6382ea6094000000000000000000000000000000000000000080b244432d4c353a4e2b4265736a3770445a46784f6149703630735163757a382f4f672b617361655a3673376543676b6245493d26a04904c712736ce12808f531996007d3eb1c1e1c1dcf5431f6252678b626385e40a043ead01a06044cd86fba04ae1dc5259c5b3b5556a8bd86aeb8867e8f1e41512a' 26 | } 27 | }; 28 | 29 | describe('ERC20 Tests', function() { 30 | let txid = ''; 31 | const currency = 'ERC20'; 32 | const currencyConfig = config.currencyConfig; 33 | const rpcs = new CryptoRpc(config, currencyConfig); 34 | 35 | 36 | this.timeout(10000); 37 | 38 | before(done => { 39 | setTimeout(done, 5000); 40 | }); 41 | 42 | it('should be able to get a balance', async () => { 43 | const balance = await rpcs.getBalance({ currency }); 44 | assert(balance != undefined); 45 | }); 46 | 47 | it('should be able to send a transaction', async () => { 48 | txid = await rpcs.unlockAndSendToAddress({ currency, address: config.currencyConfig.sendTo, amount: '10000', passphrase: currencyConfig.unlockPassword }); 49 | assert(txid); 50 | }); 51 | 52 | it('should be able to send a transaction and specify a custom nonce and gasPrice', async () => { 53 | txid = await rpcs.unlockAndSendToAddress({ 54 | currency, 55 | address: config.currencyConfig.sendTo, 56 | amount: '10000', 57 | passphrase: currencyConfig.unlockPassword, 58 | gasPrice: 30000000000, 59 | nonce: 24 60 | }); 61 | let decodedParams = await rpcs.getTransaction({ txid }); 62 | expect(decodedParams.nonce).to.equal(24); 63 | expect(decodedParams.gasPrice).to.equal('30000000000'); 64 | assert.isTrue(util.isHex(txid)); 65 | }); 66 | 67 | it('should be able to send a big transaction', async () => { 68 | txid = await rpcs.unlockAndSendToAddress({ currency, address: config.currencyConfig.sendTo, amount: 1e23, passphrase: currencyConfig.unlockPassword }); 69 | assert(txid); 70 | }); 71 | 72 | it('should be able to send many transactions', async () => { 73 | const address = config.currencyConfig.sendTo; 74 | const amount = '1000'; 75 | const payToArray = [{ address, amount }, {address, amount}]; 76 | const eventEmitter = rpcs.rpcs.ERC20.emitter; 77 | let eventCounter = 0; 78 | let emitResults = []; 79 | const emitPromise = new Promise(resolve => { 80 | eventEmitter.on('success', (emitData) => { 81 | eventCounter++; 82 | emitResults.push(emitData); 83 | if (eventCounter === 2) { 84 | resolve(emitResults); 85 | } 86 | }); 87 | }); 88 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 89 | currency, 90 | payToArray, 91 | passphrase: currencyConfig.unlockPassword 92 | }); 93 | await emitPromise; 94 | assert(emitResults[0].txid); 95 | expect(emitResults[0].error === null); 96 | expect(emitResults[0].address === address); 97 | expect(emitResults[0].amount === amount); 98 | assert(emitResults[1].txid); 99 | expect(emitResults[1].error === null); 100 | expect(emitResults[1].address === address); 101 | expect(emitResults[1].amount === amount); 102 | assert.isTrue(outputArray.length === 2); 103 | assert.isTrue(util.isHex(outputArray[0].txid)); 104 | assert.isTrue(util.isHex(outputArray[1].txid)); 105 | expect(outputArray[0].txid).to.have.lengthOf(66); 106 | expect(outputArray[1].txid).to.have.lengthOf(66); 107 | }); 108 | 109 | it('should reject when one of many transactions fails', async () => { 110 | const address = config.currencyConfig.sendTo; 111 | const amount = '1000'; 112 | const payToArray = [ 113 | { address, amount }, 114 | { address: 'funkyColdMedina', amount: 1 } 115 | ]; 116 | const eventEmitter = rpcs.rpcs.ERC20.emitter; 117 | let emitResults = []; 118 | const emitPromise = new Promise(resolve => { 119 | eventEmitter.on('failure', (emitData) => { 120 | emitResults.push(emitData); 121 | resolve(); 122 | }); 123 | }); 124 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 125 | currency, 126 | payToArray, 127 | passphrase: currencyConfig.unlockPassword 128 | }); 129 | await emitPromise; 130 | assert(!outputArray[1].txid); 131 | expect(outputArray[1].error).to.equal(emitResults[0].error); 132 | expect(emitResults.length).to.equal(1); 133 | assert(emitResults[0].error); 134 | }); 135 | 136 | it('should be able to decode a non ERC-20 raw transaction', async () => { 137 | const { rawTx } = config.currencyConfig; 138 | assert(rawTx); 139 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 140 | assert(decoded); 141 | assert(!decoded.decodedData); 142 | }); 143 | 144 | it('should be able to decode a raw ERC-20 transaction', async () => { 145 | const rawTx = '0xf86c118459682f0083027100949c9933a9258347db795ade131c93d1c5ae53438980b844a9059cbb0000000000000000000000007ee308b49e36ab516cd0186b3a47cfd31d2499a100000000000000000000000000000000000000000000000000f4a6889d2aeff6830138818080'; 146 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 147 | assert(decoded); 148 | assert(decoded.decodedData); 149 | expect(decoded.decodedData.args._to).to.equal('0x7ee308b49e36Ab516cd0186B3a47CFD31d2499A1'); 150 | expect(Number(decoded.decodedData.args._value)).to.equal(68862999999999990); 151 | }); 152 | }); 153 | 154 | -------------------------------------------------------------------------------- /tests/eth.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const {assert, expect} = require('chai'); 3 | const mocha = require('mocha'); 4 | const { before, describe, it } = mocha; 5 | const ethers = require('ethers'); 6 | const util = require('web3-utils'); 7 | const sinon = require('sinon'); 8 | const config = { 9 | chain: 'ETH', 10 | host: process.env.HOST_ETH || 'geth', 11 | protocol: 'http', 12 | port: '8545', 13 | rpcPort: '8545', 14 | account: '0x00a329c0648769A73afAc7F9381E08FB43dBEA72', 15 | currencyConfig: { 16 | sendTo: '0xA15035277A973d584b1d6150e93C21152D6Af440', 17 | unlockPassword: '', 18 | privateKey: 19 | '4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7', 20 | rawTx: 21 | '0xf8978202e38471a14e6382ea6094000000000000000000000000000000000000000080b244432d4c353a4e2b4265736a3770445a46784f6149703630735163757a382f4f672b617361655a3673376543676b6245493d26a04904c712736ce12808f531996007d3eb1c1e1c1dcf5431f6252678b626385e40a043ead01a06044cd86fba04ae1dc5259c5b3b5556a8bd86aeb8867e8f1e41512a' 22 | } 23 | }; 24 | 25 | describe('ETH Tests', function() { 26 | const currency = 'ETH'; 27 | const currencyConfig = config.currencyConfig; 28 | const rpcs = new CryptoRpc(config, currencyConfig); 29 | const ethRPC = rpcs.get(currency); 30 | let txid = ''; 31 | let blockHash = ''; 32 | 33 | this.timeout(10000); 34 | 35 | before(done => { 36 | setTimeout(done, 5000); 37 | }); 38 | 39 | afterEach(() => { 40 | sinon.restore(); 41 | }); 42 | 43 | it('should estimate gas price', async () => { 44 | sinon.spy(ethRPC.web3.eth, 'getBlock'); 45 | sinon.spy(ethRPC.blockGasPriceCache, 'get'); 46 | const gasPrice = await ethRPC.estimateGasPrice(); 47 | assert.isDefined(gasPrice); 48 | expect(gasPrice).to.be.gt(0); 49 | expect(ethRPC.blockGasPriceCache.get.callCount).to.equal(0); 50 | expect(ethRPC.web3.eth.getBlock.callCount).to.equal(10); 51 | }); 52 | 53 | it('should estimate gas price with cache', async () => { 54 | sinon.spy(ethRPC.web3.eth, 'getBlock'); 55 | sinon.spy(ethRPC.blockGasPriceCache, 'get'); 56 | const gasPrice = await ethRPC.estimateGasPrice(); 57 | assert.isDefined(gasPrice); 58 | expect(gasPrice).to.be.gt(0); 59 | expect(ethRPC.blockGasPriceCache.get.callCount).to.be.gt(0); 60 | expect(ethRPC.web3.eth.getBlock.callCount).to.be.lt(10); 61 | }); 62 | 63 | it('should estimate fee for type 2 transaction', async () => { 64 | sinon.spy(ethRPC.web3.eth, 'getBlock'); 65 | let maxFee = await ethRPC.estimateFee({txType: 2, priority: 5}); 66 | assert.isDefined(maxFee); 67 | expect(maxFee).to.be.equal(5154455240); 68 | expect(ethRPC.web3.eth.getBlock.callCount).to.equal(1); 69 | }); 70 | 71 | it('should estimate max fee', async () => { 72 | sinon.spy(ethRPC.web3.eth, 'getBlock'); 73 | let maxFee = await ethRPC.estimateMaxFee({}); 74 | assert.isDefined(maxFee); 75 | expect(maxFee).to.be.equal(2654455240); 76 | expect(ethRPC.web3.eth.getBlock.callCount).to.equal(1); 77 | }); 78 | 79 | it('should estimate max fee using priority fee percentile', async () => { 80 | sinon.spy(ethRPC.emitter, 'emit'); 81 | sinon.spy(ethRPC.web3.eth, 'getBlock'); 82 | let maxFee = await ethRPC.estimateMaxFee({ percentile: 15 }); 83 | assert.isDefined(maxFee); 84 | expect(maxFee).to.be.equal(1154455240); 85 | expect(ethRPC.web3.eth.getBlock.callCount).to.be.lt(10); 86 | expect(ethRPC.emitter.emit.callCount).to.equal(0); 87 | }); 88 | 89 | it('should estimate max priority fee', async () => { 90 | sinon.spy(ethRPC.blockMaxPriorityFeeCache, 'set'); 91 | const maxPriorityFee = await ethRPC.estimateMaxPriorityFee({}); 92 | assert.isDefined(maxPriorityFee); 93 | expect(maxPriorityFee).to.be.gt(0); 94 | expect(maxPriorityFee).to.be.equal(1000000000); 95 | expect(ethRPC.blockMaxPriorityFeeCache.set.callCount).to.equal(0); 96 | }); 97 | 98 | it('should estimate fee', async () => { 99 | const fee = await rpcs.estimateFee({ currency, nBlocks: 4 }); 100 | assert.isTrue(fee === 20000000000); 101 | }); 102 | 103 | it('should send raw transaction', async () => { 104 | // Get nonce 105 | const txCount = await rpcs.getTransactionCount({ 106 | currency, 107 | address: config.account 108 | }); 109 | 110 | // construct the transaction data 111 | const txData = { 112 | nonce: txCount, 113 | chainId: 1337, 114 | gasLimit: 25000, 115 | gasPrice: 2.1*10e9, 116 | to: config.currencyConfig.sendTo, 117 | value: Number(util.toWei('123', 'wei')) 118 | }; 119 | const privateKey = Buffer.from(config.currencyConfig.privateKey, 'hex'); 120 | const signer = new ethers.Wallet(privateKey); 121 | const signedTx = await signer.signTransaction(txData); 122 | const sentTx = await rpcs.sendRawTransaction({ 123 | currency, 124 | rawTx: signedTx 125 | }); 126 | expect(sentTx.length).to.equal(66); 127 | }); 128 | 129 | it('should catch failed send raw transaction', async () => { 130 | try { 131 | // construct the transaction data 132 | const txData = { 133 | nonce: null, 134 | chainId: 1337, 135 | gasLimit: 25000, 136 | gasPrice: 2.1*10e9, 137 | to: config.currencyConfig.sendTo, 138 | value: Number(util.toWei('123', 'wei')) 139 | }; 140 | const privateKey = Buffer.from(config.currencyConfig.privateKey, 'hex'); 141 | const signer = new ethers.Wallet(privateKey); 142 | const signedTx = await signer.signTransaction(txData); 143 | await rpcs.sendRawTransaction({ 144 | currency, 145 | rawTx: signedTx 146 | }); 147 | return signedTx; 148 | } catch(err) { 149 | expect(err.message).to.include('nonce too low'); 150 | } 151 | }); 152 | 153 | it('should succeed send raw transaction already broadcast', async () => { 154 | const txCount = await rpcs.getTransactionCount({ 155 | currency, 156 | address: config.account 157 | }); 158 | try { 159 | // construct the transaction data 160 | const txData = { 161 | // add to nonce so that the first tx isn't auto-mined before second tx is sent 162 | nonce: txCount + 1, 163 | chainId: 1337, 164 | gasLimit: 25000, 165 | gasPrice: 2.1*10e9, 166 | to: config.currencyConfig.sendTo, 167 | value: Number(util.toWei('123', 'wei')) 168 | }; 169 | const privateKey = Buffer.from(config.currencyConfig.privateKey, 'hex'); 170 | const signer = new ethers.Wallet(privateKey); 171 | const signedTx = await signer.signTransaction(txData); 172 | const txSend1 = await rpcs.sendRawTransaction({ 173 | currency, 174 | rawTx: signedTx 175 | }); 176 | const txSend2 = await rpcs.sendRawTransaction({ 177 | currency, 178 | rawTx: signedTx 179 | }); 180 | expect(txSend1).to.equal(txSend2); 181 | } catch(err) { 182 | expect(err.toString()).to.not.exist(); 183 | } 184 | }); 185 | 186 | it('should succeed send raw type 2 transaction', async () => { 187 | const txCount = await rpcs.getTransactionCount({ 188 | currency, 189 | address: config.account 190 | }); 191 | try { 192 | // construct the transaction data 193 | const txData = { 194 | nonce: txCount, 195 | chainId: 1337, 196 | gasLimit: 25000, 197 | type: 2, 198 | maxFeePerGas: Number(util.toWei('10', 'gwei')), 199 | to: config.currencyConfig.sendTo, 200 | value: Number(util.toWei('321', 'wei')) 201 | }; 202 | const privateKey = Buffer.from(config.currencyConfig.privateKey, 'hex'); 203 | const signer = new ethers.Wallet(privateKey); 204 | const signedTx = await signer.signTransaction(txData); 205 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx: signedTx }); 206 | assert.isDefined(decoded); 207 | assert.isObject(decoded); 208 | expect(decoded.type).to.equal(2); 209 | const txSend1 = await rpcs.sendRawTransaction({ 210 | currency, 211 | rawTx: signedTx 212 | }); 213 | expect(txSend1).to.equal('0x94266a12747ccea60d7566777d22c8e3b7bbaa71e16e69468c547c2bab0b9f90'); 214 | } catch(err) { 215 | expect(err.toString()).to.not.exist(); 216 | } 217 | }); 218 | 219 | it('should be able to get a block hash', async () => { 220 | const block = await rpcs.getBestBlockHash({ currency }); 221 | blockHash = block; 222 | assert.isTrue(util.isHex(block)); 223 | }); 224 | 225 | it('should get block', async () => { 226 | const reqBlock = await rpcs.getBlock({ currency, hash: blockHash }); 227 | assert(reqBlock.hash === blockHash); 228 | expect(reqBlock).to.have.property('number'); 229 | expect(reqBlock).to.have.property('hash'); 230 | expect(reqBlock).to.have.property('parentHash'); 231 | expect(reqBlock).to.have.property('sha3Uncles'); 232 | expect(reqBlock).to.have.property('logsBloom'); 233 | expect(reqBlock).to.have.property('transactionsRoot'); 234 | expect(reqBlock).to.have.property('stateRoot'); 235 | expect(reqBlock).to.have.property('receiptsRoot'); 236 | expect(reqBlock).to.have.property('miner'); 237 | expect(reqBlock).to.have.property('difficulty'); 238 | expect(reqBlock).to.have.property('totalDifficulty'); 239 | expect(reqBlock).to.have.property('extraData'); 240 | expect(reqBlock).to.have.property('size'); 241 | expect(reqBlock).to.have.property('gasLimit'); 242 | expect(reqBlock).to.have.property('gasUsed'); 243 | expect(reqBlock).to.have.property('timestamp'); 244 | expect(reqBlock).to.have.property('transactions'); 245 | expect(reqBlock).to.have.property('uncles'); 246 | }); 247 | 248 | it('should be able to get a balance', async () => { 249 | const balance = await rpcs.getBalance({ currency }); 250 | assert(util.isAddress(balance[0].account)); 251 | assert.hasAllKeys(balance[0], ['account', 'balance']); 252 | }); 253 | 254 | it('should be able to send a transaction', async () => { 255 | txid = await rpcs.unlockAndSendToAddress({ 256 | currency, 257 | address: config.currencyConfig.sendTo, 258 | amount: '10000', 259 | passphrase: currencyConfig.unlockPassword 260 | }); 261 | assert.isTrue(util.isHex(txid)); 262 | }); 263 | 264 | it('should be able to send a transaction and specify a custom nonce and gasPrice', async () => { 265 | txid = await rpcs.unlockAndSendToAddress({ 266 | currency, 267 | address: config.currencyConfig.sendTo, 268 | amount: '10000', 269 | passphrase: currencyConfig.unlockPassword, 270 | gasPrice: 30000000000, 271 | nonce: 25, 272 | chainId: 1337 273 | }); 274 | let decodedParams = await rpcs.getTransaction({ txid }); 275 | expect(decodedParams.nonce).to.equal(25); 276 | expect(decodedParams.gasPrice).to.equal('30000000000'); 277 | assert.isTrue(util.isHex(txid)); 278 | }); 279 | 280 | it('should be able to send many transactions', async () => { 281 | const address = config.currencyConfig.sendTo; 282 | const amount = '1000'; 283 | const payToArray = [{ address, amount }, {address, amount}]; 284 | const eventEmitter = rpcs.rpcs.ETH.emitter; 285 | let eventCounter = 0; 286 | let emitResults = []; 287 | const emitPromise = new Promise(resolve => { 288 | eventEmitter.on('success', (emitData) => { 289 | eventCounter++; 290 | emitResults.push(emitData); 291 | if (eventCounter === 2) { 292 | resolve(emitResults); 293 | } 294 | }); 295 | }); 296 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 297 | currency, 298 | payToArray, 299 | passphrase: currencyConfig.unlockPassword 300 | }); 301 | await emitPromise; 302 | assert(emitResults[0].txid); 303 | expect(emitResults[0].error === null); 304 | expect(emitResults[0].address === address); 305 | expect(emitResults[0].amount === amount); 306 | assert(emitResults[1].txid); 307 | expect(emitResults[1].error === null); 308 | expect(emitResults[1].address === address); 309 | expect(emitResults[1].amount === amount); 310 | assert.isTrue(outputArray.length === 2); 311 | assert.isTrue(util.isHex(outputArray[0].txid)); 312 | assert.isTrue(util.isHex(outputArray[1].txid)); 313 | expect(outputArray[0].txid).to.have.lengthOf(66); 314 | expect(outputArray[1].txid).to.have.lengthOf(66); 315 | expect(outputArray[1].txid).to.not.equal(outputArray[0].txid); 316 | }); 317 | 318 | it('should reject when one of many transactions fails', async () => { 319 | const address = config.currencyConfig.sendTo; 320 | const amount = '1000'; 321 | const payToArray = [ 322 | { address, amount }, 323 | { address: 'funkyColdMedina', amount: 1 } 324 | ]; 325 | const eventEmitter = rpcs.rpcs.ETH.emitter; 326 | let emitResults = []; 327 | const emitPromise = new Promise(resolve => { 328 | eventEmitter.on('failure', (emitData) => { 329 | emitResults.push(emitData); 330 | resolve(); 331 | }); 332 | }); 333 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 334 | currency, 335 | payToArray, 336 | passphrase: currencyConfig.unlockPassword 337 | }); 338 | await emitPromise; 339 | assert(!outputArray[1].txid); 340 | expect(outputArray[1].error).to.equal(emitResults[0].error); 341 | expect(emitResults.length).to.equal(1); 342 | assert(emitResults[0].error); 343 | }); 344 | 345 | it('should be able to get a transaction', async () => { 346 | const tx = await rpcs.getTransaction({ currency, txid }); 347 | assert.isDefined(tx); 348 | assert.isObject(tx); 349 | }); 350 | 351 | it('should be able to decode a raw transaction', async () => { 352 | const { rawTx } = config.currencyConfig; 353 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 354 | assert.isDefined(decoded); 355 | }); 356 | 357 | it('should be able to decode a raw type 2 transaction', async () => { 358 | const rawTx = '0x02f9017d0580808504a817c800809437d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a870dd764300b8000b90152f9014f808504a817c800809437d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a870dd764300b8000b90124b6b4af05000000000000000000000000000000000000000000000000000dd764300b800000000000000000000000000000000000000000000000000000000004a817c8000000000000000000000000000000000000000000000000000000016ada606a26050bb49a5a8228599e0dd48c1368abd36f4f14d2b74a015b2d168dbcab0773ce399393220df874bb22ca961f351e038acd2ba5cc8c764385c9f23707622cc435000000000000000000000000000000000000000000000000000000000000001c7e247d684a635813267b10a63f7f3ba88b28ca2790c909110b28236cf1b9bba03451e83d5834189f28d4c77802fc76b7c760a42bc8bebf8dd15e6ead146805630000000000000000000000000000000000000000000000000000000000000000058080c0'; 359 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 360 | assert.isDefined(decoded); 361 | assert.isObject(decoded); 362 | expect(decoded.type).to.equal(2); 363 | assert.isDefined(decoded.maxFeePerGas); 364 | assert.isDefined(decoded.maxPriorityFeePerGas); 365 | }); 366 | 367 | it('should get the tip', async () => { 368 | const tip = await rpcs.getTip({ currency }); 369 | assert.hasAllKeys(tip, ['height', 'hash']); 370 | }); 371 | 372 | it('should get confirmations', async () => { 373 | const confirmations = await rpcs.getConfirmations({ currency, txid }); 374 | assert.isDefined(confirmations); 375 | }); 376 | 377 | it('should not get confirmations with invalid txid', async () => { 378 | try { 379 | await rpcs.getConfirmations({ currency, txid: 'wrongtxid' }); 380 | } catch (err) { 381 | assert.isDefined(err); 382 | } 383 | }); 384 | 385 | it('should validate address', async () => { 386 | const isValid = await rpcs.validateAddress({ 387 | currency, 388 | address: config.currencyConfig.sendTo 389 | }); 390 | const utilVaildate = util.isAddress(config.currencyConfig.sendTo); 391 | assert.isTrue(isValid === utilVaildate); 392 | }); 393 | 394 | it('should not validate bad address', async () => { 395 | const isValid = await rpcs.validateAddress({ 396 | currency, 397 | address: 'NOTANADDRESS' 398 | }); 399 | const utilVaildate = util.isAddress('NOTANADDRESS'); 400 | assert.isTrue(isValid === utilVaildate); 401 | }); 402 | 403 | it('should be able to get server info', async () => { 404 | const info = await rpcs.getServerInfo({ currency }); 405 | expect(typeof info).to.equal('string'); 406 | }); 407 | 408 | it('should get pending transactions', async () => { 409 | const pendingTxs = await rpcs.getTransactions({ currency }); 410 | expect(Array.isArray(pendingTxs)).to.equal(true); 411 | }); 412 | }); 413 | -------------------------------------------------------------------------------- /tests/evm.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const {assert, expect} = require('chai'); 3 | const mocha = require('mocha'); 4 | const sinon = require('sinon'); 5 | const { before, describe, it } = mocha; 6 | const ethers = require('ethers'); 7 | const util = require('web3-utils'); 8 | const chainConfig = require('../lib/eth/chains'); 9 | 10 | const configs = [ 11 | { 12 | chain: 'ARB', 13 | host: process.env.HOST_ARB || 'ganache', 14 | protocol: 'http', 15 | port: '8545', 16 | rpcPort: '8545', 17 | account: '0x30aEB843945055c9d96c4f2E99BF66FF1EF778C7', 18 | currencyConfig: { 19 | sendTo: '0xBa1E702D95682023782DD630fdC66E13ded26615', 20 | unlockPassword: '', 21 | privateKey: '0x3669381038794f93b2e30f9fc7edc871aec5351e40af833aa049e4c00a25ec8a', 22 | rawTx: 23 | '0xf8978202e38471a14e6382ea6094000000000000000000000000000000000000000080b244432d4c353a4e2b4265736a3770445a46784f6149703630735163757a382f4f672b617361655a3673376543676b6245493d26a04904c712736ce12808f531996007d3eb1c1e1c1dcf5431f6252678b626385e40a043ead01a06044cd86fba04ae1dc5259c5b3b5556a8bd86aeb8867e8f1e41512a' 24 | }, 25 | isEVM: true 26 | }, 27 | { 28 | chain: 'OP', 29 | host: process.env.HOST_OP || 'ganache', 30 | protocol: 'http', 31 | port: '8545', 32 | rpcPort: '8545', 33 | account: '0x94BE9Bd3f76B0689a141aED24c149bB6acBa5411', 34 | currencyConfig: { 35 | sendTo: '0xe4Fcbfb1c2ddD20d618CDD8E78d8E64aCB835AD0', 36 | unlockPassword: '', 37 | privateKey: '0xbc65cb6c016e4e05d56ea272dd2513ab8fb999f85badbd726e2db7e12383b748', 38 | rawTx: 39 | '0xf8978202e38471a14e6382ea6094000000000000000000000000000000000000000080b244432d4c353a4e2b4265736a3770445a46784f6149703630735163757a382f4f672b617361655a3673376543676b6245493d26a04904c712736ce12808f531996007d3eb1c1e1c1dcf5431f6252678b626385e40a043ead01a06044cd86fba04ae1dc5259c5b3b5556a8bd86aeb8867e8f1e41512a' 40 | }, 41 | isEVM: true 42 | }, 43 | { 44 | chain: 'BASE', 45 | host: process.env.HOST_BASE || 'ganache', 46 | protocol: 'http', 47 | port: '8545', 48 | rpcPort: '8545', 49 | account: '0xB556dc491B7652f73B9D3080A6Cbf2766dB368e9', 50 | currencyConfig: { 51 | sendTo: '0x37D3bCDA9d7d5Dc41e32DAd77a5BA89a77aA8BD0', 52 | unlockPassword: '', 53 | privateKey: '0x61cad5947d07d2ca69fc57e96c5b79b2927ea263475b17938b2900d0a258faec', 54 | rawTx: 55 | '0xf8978202e38471a14e6382ea6094000000000000000000000000000000000000000080b244432d4c353a4e2b4265736a3770445a46784f6149703630735163757a382f4f672b617361655a3673376543676b6245493d26a04904c712736ce12808f531996007d3eb1c1e1c1dcf5431f6252678b626385e40a043ead01a06044cd86fba04ae1dc5259c5b3b5556a8bd86aeb8867e8f1e41512a' 56 | }, 57 | isEVM: true 58 | }, 59 | { 60 | chain: 'MATIC', 61 | host: process.env.HOST_MATIC || 'ganache', 62 | protocol: 'http', 63 | port: '8545', 64 | rpcPort: '8545', 65 | account: '0xf9A09F3Dd46D475B59A9Db149Aca5654A9040E07', 66 | currencyConfig: { 67 | sendTo: '0xc0b4dD3941898CB1dAF5cD768Bc1997F77a3D9a5', 68 | unlockPassword: '', 69 | privateKey: 70 | '0x1ac9e617ee805e0e6fab5aff99b960bf464d03e8db5bc73e15419422a81c57e2', 71 | rawTx: 72 | '0xf8978202e38471a14e6382ea6094000000000000000000000000000000000000000080b244432d4c353a4e2b4265736a3770445a46784f6149703630735163757a382f4f672b617361655a3673376543676b6245493d26a04904c712736ce12808f531996007d3eb1c1e1c1dcf5431f6252678b626385e40a043ead01a06044cd86fba04ae1dc5259c5b3b5556a8bd86aeb8867e8f1e41512a' 73 | }, 74 | isEVM: true 75 | } 76 | ]; 77 | 78 | configs.forEach((config) => { 79 | describe(`${config.chain} Tests: `, function() { 80 | const currency = config.chain; 81 | const currencyConfig = config.currencyConfig; 82 | const rpcs = new CryptoRpc(config, currencyConfig); 83 | const evmRPC = rpcs.get(currency); 84 | let txid = ''; 85 | let blockHash = ''; 86 | 87 | this.timeout(30000); 88 | 89 | before(done => { 90 | setTimeout(done, 10000); 91 | }); 92 | 93 | afterEach(() => { 94 | sinon.restore(); 95 | }); 96 | 97 | it('should estimate fee', async () => { 98 | const fee = await rpcs.estimateFee({ currency, nBlocks: 4 }); 99 | assert.isDefined(fee); 100 | expect(fee).to.be.gte(20000000000); 101 | }); 102 | 103 | it('should send raw transaction', async () => { 104 | // construct the transaction data 105 | const txData = { 106 | nonce: 0, 107 | gasLimit: 25000, 108 | gasPrice: 2.1*10e9, 109 | to: config.currencyConfig.sendTo, 110 | value: Number(util.toWei('123', 'wei')) 111 | }; 112 | const privateKey = config.currencyConfig.privateKey; 113 | const signer = new ethers.Wallet(privateKey); 114 | const signedTx = await signer.signTransaction(txData); 115 | const sentTx = await rpcs.sendRawTransaction({ 116 | currency, 117 | rawTx: signedTx 118 | }); 119 | expect(sentTx.length).to.equal(66); 120 | }); 121 | 122 | it('should catch failed send raw transaction', async () => { 123 | try { 124 | // construct the transaction data 125 | const txData = { 126 | nonce: 1, 127 | gasLimit: 25000, 128 | gasPrice: 2.1*10e9, 129 | to: config.currencyConfig.sendTo, 130 | value: Number(util.toWei('123', 'wei')) 131 | }; 132 | const privateKey = config.currencyConfig.privateKey; 133 | const signer = new ethers.Wallet(privateKey); 134 | const signedTx = await signer.signTransaction(txData); 135 | await rpcs.sendRawTransaction({ 136 | currency, 137 | rawTx: signedTx 138 | }); 139 | } catch(err) { 140 | expect(err.message).to.include('Transaction nonce is too low'); 141 | } 142 | }); 143 | 144 | it('should estimate fee for type 2 transaction', async () => { 145 | sinon.spy(evmRPC.web3.eth, 'getBlock'); 146 | let maxFee = await evmRPC.estimateFee({txType: 2, priority: 5}); 147 | assert.isDefined(maxFee); 148 | expect(maxFee).to.be.equal(5000000000); 149 | expect(evmRPC.web3.eth.getBlock.callCount).to.equal(1); 150 | }); 151 | 152 | it('should use fee minimums when estimating priority fee for type 2 txs', async () => { 153 | const maxFee = await evmRPC.estimateMaxPriorityFee({ percentile: 25 }); 154 | const minimumFee = chainConfig[config.chain] ? chainConfig[config.chain].priorityFee : 2.5; 155 | assert.isDefined(maxFee); 156 | expect(maxFee).to.be.equal(minimumFee * 1e9); 157 | }); 158 | 159 | it('should estimate gas price', async () => { 160 | const gasPrice = await evmRPC.estimateGasPrice(); 161 | assert.isDefined(gasPrice); 162 | expect(gasPrice).to.be.gte(20000000000); 163 | }); 164 | 165 | it('should be able to get a block hash', async () => { 166 | const block = await rpcs.getBestBlockHash({ currency }); 167 | blockHash = block; 168 | assert.isTrue(util.isHex(block)); 169 | }); 170 | 171 | it('should get block', async () => { 172 | const reqBlock = await rpcs.getBlock({ currency, hash: blockHash }); 173 | assert(reqBlock.hash === blockHash); 174 | expect(reqBlock).to.have.property('number'); 175 | expect(reqBlock).to.have.property('hash'); 176 | expect(reqBlock).to.have.property('parentHash'); 177 | expect(reqBlock).to.have.property('sha3Uncles'); 178 | expect(reqBlock).to.have.property('logsBloom'); 179 | expect(reqBlock).to.have.property('transactionsRoot'); 180 | expect(reqBlock).to.have.property('stateRoot'); 181 | expect(reqBlock).to.have.property('receiptsRoot'); 182 | expect(reqBlock).to.have.property('miner'); 183 | expect(reqBlock).to.have.property('difficulty'); 184 | expect(reqBlock).to.have.property('totalDifficulty'); 185 | expect(reqBlock).to.have.property('extraData'); 186 | expect(reqBlock).to.have.property('size'); 187 | expect(reqBlock).to.have.property('gasLimit'); 188 | expect(reqBlock).to.have.property('gasUsed'); 189 | expect(reqBlock).to.have.property('timestamp'); 190 | expect(reqBlock).to.have.property('transactions'); 191 | expect(reqBlock).to.have.property('uncles'); 192 | }); 193 | 194 | it('should be able to get a balance', async () => { 195 | const balance = await rpcs.getBalance({ currency }); 196 | assert(util.isAddress(balance[0].account)); 197 | assert.hasAllKeys(balance[0], ['account', 'balance']); 198 | }); 199 | 200 | it('should be able to send a transaction', async () => { 201 | txid = await rpcs.unlockAndSendToAddress({ 202 | currency, 203 | address: config.currencyConfig.sendTo, 204 | amount: '10000', 205 | passphrase: currencyConfig.unlockPassword 206 | }); 207 | assert.isTrue(util.isHex(txid)); 208 | }); 209 | 210 | it('should be able to send a transaction and specify a custom gasPrice', async () => { 211 | txid = await rpcs.unlockAndSendToAddress({ 212 | currency, 213 | address: config.currencyConfig.sendTo, 214 | amount: '10000', 215 | passphrase: currencyConfig.unlockPassword, 216 | gasPrice: 30000000000 217 | }); 218 | let decodedParams = await rpcs.getTransaction({ txid }); 219 | expect(decodedParams.gasPrice).to.equal('30000000000'); 220 | assert.isTrue(util.isHex(txid)); 221 | }); 222 | 223 | it('should be able to send many transactions', async () => { 224 | const address = config.currencyConfig.sendTo; 225 | const amount = '1000'; 226 | const payToArray = [{ address, amount }, {address, amount}]; 227 | const eventEmitter = rpcs.rpcs[config.chain].emitter; 228 | let eventCounter = 0; 229 | let emitResults = []; 230 | const emitPromise = new Promise(resolve => { 231 | eventEmitter.on('success', (emitData) => { 232 | eventCounter++; 233 | emitResults.push(emitData); 234 | if (eventCounter === 2) { 235 | resolve(emitResults); 236 | } 237 | }); 238 | }); 239 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 240 | currency, 241 | payToArray, 242 | passphrase: currencyConfig.unlockPassword 243 | }); 244 | await emitPromise; 245 | assert(emitResults[0].txid); 246 | expect(emitResults[0].error === null); 247 | expect(emitResults[0].address === address); 248 | expect(emitResults[0].amount === amount); 249 | assert(emitResults[1].txid); 250 | expect(emitResults[1].error === null); 251 | expect(emitResults[1].address === address); 252 | expect(emitResults[1].amount === amount); 253 | assert.isTrue(outputArray.length === 2); 254 | assert.isTrue(util.isHex(outputArray[0].txid)); 255 | assert.isTrue(util.isHex(outputArray[1].txid)); 256 | expect(outputArray[0].txid).to.have.lengthOf(66); 257 | expect(outputArray[1].txid).to.have.lengthOf(66); 258 | expect(outputArray[1].txid).to.not.equal(outputArray[0].txid); 259 | }); 260 | 261 | it('should reject when one of many transactions fails', async () => { 262 | const address = config.currencyConfig.sendTo; 263 | const amount = '1000'; 264 | const payToArray = [ 265 | { address, amount }, 266 | { address: 'funkyColdMedina', amount: 1 } 267 | ]; 268 | const eventEmitter = rpcs.rpcs[config.chain].emitter; 269 | let emitResults = []; 270 | const emitPromise = new Promise(resolve => { 271 | eventEmitter.on('failure', (emitData) => { 272 | emitResults.push(emitData); 273 | resolve(); 274 | }); 275 | }); 276 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 277 | currency, 278 | payToArray, 279 | passphrase: currencyConfig.unlockPassword 280 | }); 281 | await emitPromise; 282 | assert(!outputArray[1].txid); 283 | expect(outputArray[1].error).to.equal(emitResults[0].error); 284 | expect(emitResults.length).to.equal(1); 285 | assert(emitResults[0].error); 286 | }); 287 | 288 | it('should be able to get a transaction', async () => { 289 | const tx = await rpcs.getTransaction({ currency, txid }); 290 | assert.isDefined(tx); 291 | assert.isObject(tx); 292 | }); 293 | 294 | it('should be able to decode a raw transaction', async () => { 295 | const { rawTx } = config.currencyConfig; 296 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 297 | assert.isDefined(decoded); 298 | }); 299 | 300 | it('should get the tip', async () => { 301 | const tip = await rpcs.getTip({ currency }); 302 | assert.hasAllKeys(tip, ['height', 'hash']); 303 | }); 304 | 305 | it('should get confirmations', async () => { 306 | const confirmations = await rpcs.getConfirmations({ currency, txid }); 307 | assert.isDefined(confirmations); 308 | }); 309 | 310 | it('should not get confirmations with invalid txid', async () => { 311 | try { 312 | await rpcs.getConfirmations({ currency, txid: 'wrongtxid' }); 313 | } catch (err) { 314 | assert.isDefined(err); 315 | } 316 | }); 317 | 318 | it('should validate address', async () => { 319 | const isValid = await rpcs.validateAddress({ 320 | currency, 321 | address: config.currencyConfig.sendTo 322 | }); 323 | const utilVaildate = util.isAddress(config.currencyConfig.sendTo); 324 | assert.isTrue(isValid === utilVaildate); 325 | }); 326 | 327 | it('should not validate bad address', async () => { 328 | const isValid = await rpcs.validateAddress({ 329 | currency, 330 | address: 'NOTANADDRESS' 331 | }); 332 | const utilVaildate = util.isAddress('NOTANADDRESS'); 333 | assert.isTrue(isValid === utilVaildate); 334 | }); 335 | }); 336 | }); 337 | -------------------------------------------------------------------------------- /tests/lnd.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const mocha = require('mocha'); 3 | const fs = require('fs'); 4 | // const sinon = require('sinon'); 5 | const { expect } = require('chai'); 6 | const assert = require('assert'); 7 | const { describe, it } = mocha; 8 | 9 | const config1 = { 10 | chain: 'LNBTC', 11 | rpcPort: '11009', 12 | host: '172.28.0.5', 13 | cert: '' 14 | }; 15 | const config2 = { 16 | chain: 'LNBTC', 17 | rpcPort: '11010', 18 | host: '172.28.0.10', 19 | cert: '' 20 | }; 21 | const btcConfig = { 22 | chain: 'BTC', 23 | host: 'bitcoin', 24 | protocol: 'http', 25 | rpcPort: '8333', 26 | rpcUser: 'cryptorpc', 27 | rpcPass: 'local321', 28 | tokens: {}, 29 | currencyConfig: { 30 | sendTo: '2NGFWyW3LBPr6StDuDSNFzQF3Jouuup1rua', 31 | unlockPassword: 'password', 32 | rawTx: 33 | '0100000001641ba2d21efa8db1a08c0072663adf4c4bc3be9ee5aabb530b2d4080b8a41cca000000006a4730440220062105df71eb10b5ead104826e388303a59d5d3d134af73cdf0d5e685650f95c0220188c8a966a2d586430d84aa7624152a556550c3243baad5415c92767dcad257f0121037aaa54736c5ffa13132e8ca821be16ce4034ae79472053dde5aa4347034bc0a2ffffffff0240787d010000000017a914c8241f574dfade4d446ec90cc0e534cb120b45e387eada4f1c000000001976a9141576306b9cc227279b2a6c95c2b017bb22b0421f88ac00000000' 34 | } 35 | }; 36 | 37 | describe('LND Tests', function() { 38 | this.timeout(30000); 39 | const currency = 'LNBTC'; 40 | const passphrase = 'password'; 41 | let lightning1; 42 | let lightning2; 43 | let bitcoin; 44 | let lightningPublicKey2; 45 | let bitcoinWalletAddress; 46 | let invoice; 47 | 48 | before(async () => { 49 | // set up first lnd node 50 | const cert1 = fs.readFileSync('/root/.lnd/tls.cert'); 51 | config1.cert = Buffer.from(cert1).toString('base64'); 52 | const rpcs1 = new CryptoRpc(config1); 53 | lightning1 = rpcs1.get(currency); 54 | try { 55 | await lightning1.walletCreate({ passphrase }); 56 | await new Promise(resolve => setTimeout(resolve, 1000)); 57 | } catch (err) { 58 | if (!err[2] || !err[2].err.message.includes('wallet already exists')) { 59 | throw err; 60 | } 61 | } 62 | const macaroon1 = fs.readFileSync('/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon'); 63 | config1.macaroon = Buffer.from(macaroon1).toString('base64'); 64 | await lightning1.createNewAuthenticatedRpc(config1); 65 | 66 | 67 | // set up second lnd node 68 | const cert2 = fs.readFileSync('/root/.lnd2/tls.cert'); 69 | config2.cert = Buffer.from(cert2).toString('base64'); 70 | const rpcs2 = new CryptoRpc(config2); 71 | lightning2 = rpcs2.get(currency); 72 | try { 73 | await lightning2.walletCreate({ passphrase }); 74 | await new Promise(resolve => setTimeout(resolve, 1000)); 75 | } catch (err) { 76 | if (!err[2] || !err[2].err.message.includes('wallet already exists')) { 77 | throw err; 78 | } 79 | } 80 | const macaroon2 = fs.readFileSync('/root/.lnd2/data/chain/bitcoin/regtest/admin.macaroon'); 81 | config2.macaroon = Buffer.from(macaroon2).toString('base64'); 82 | await lightning2.createNewAuthenticatedRpc(config2); 83 | 84 | 85 | // set up btc node 86 | const { currencyConfig } = btcConfig; 87 | const bitcoinRpcs = new CryptoRpc(btcConfig, currencyConfig); 88 | bitcoin = bitcoinRpcs.get('BTC'); 89 | try { 90 | await bitcoin.asyncCall('createwallet', ['wallet0']); 91 | await bitcoin.asyncCall('encryptWallet', ['password']); 92 | } catch (err) { 93 | if (err.message.includes('Database already exists')) { 94 | await bitcoin.walletUnlock({ passphrase: btcConfig.currencyConfig.unlockPassword, time: 1000 }); 95 | } else { 96 | throw err; 97 | } 98 | } 99 | await new Promise(resolve => setTimeout(resolve, 5000)); 100 | bitcoinWalletAddress = await bitcoin.asyncCall('getnewaddress', ['wallet1']); 101 | await bitcoin.asyncCall('generatetoaddress', [120, bitcoinWalletAddress]); 102 | 103 | // wait for both lnd nodes to sync up to the btc node 104 | let syncing = true; 105 | while(syncing) { 106 | try { 107 | await new Promise(resolve => setTimeout(resolve, 500)); 108 | const walletInfo1 = await lightning1.getWalletInfo(); 109 | const walletInfo2 = await lightning2.getWalletInfo(); 110 | lightningPublicKey2 = walletInfo2.public_key; 111 | if (walletInfo1.is_synced_to_chain && walletInfo2.is_synced_to_chain) { 112 | syncing = false; 113 | } 114 | } catch (err) { 115 | if (err[2] && err[2].err.message.includes('wallet locked')) { 116 | await lightning1.walletUnlock({ passphrase }); 117 | await lightning2.walletUnlock({ passphrase }); 118 | } else { 119 | throw err; 120 | } 121 | } 122 | } 123 | 124 | await lightning1.asyncCall('addPeer', [{ socket: `${config2.host}`, public_key: lightningPublicKey2 }]); 125 | }); 126 | 127 | it('should be able to derive and fund an on chain address', async () => { 128 | const walletAddress1 = await lightning1.getBTCAddress({}); 129 | const walletAddress2 = await lightning2.getBTCAddress({}); 130 | const isValid1 = await bitcoin.validateAddress({ address: walletAddress1.address }); 131 | const isValid2 = await bitcoin.validateAddress({ address: walletAddress2.address }); 132 | expect(isValid1).to.equal(true); 133 | expect(isValid2).to.equal(true); 134 | const txid1 = await bitcoin.unlockAndSendToAddress({ currency: 'BTC', address: walletAddress1.address, amount: '500000000', passphrase: btcConfig.currencyConfig.unlockPassword }); 135 | const txid2 = await bitcoin.unlockAndSendToAddress({ currency: 'BTC', address: walletAddress1.address, amount: '500000000', passphrase: btcConfig.currencyConfig.unlockPassword }); 136 | await bitcoin.asyncCall('generatetoaddress', [6, bitcoinWalletAddress]); 137 | await new Promise(resolve => setTimeout(resolve, 1000)); 138 | expect(txid1).to.have.lengthOf(64); 139 | assert(txid1); 140 | expect(txid2).to.have.lengthOf(64); 141 | assert(txid2); 142 | }); 143 | 144 | it('should be able to create a channel', async () => { 145 | await lightning1.openChannel({ 146 | amount: 1000000, 147 | pubkey: lightningPublicKey2, 148 | socket: `${config2.host}:${config2.rpcPort}` 149 | }); 150 | const pendingChannel = await lightning1.asyncCall('getPendingChannels'); 151 | expect(pendingChannel.pending_channels.length).to.equal(1); 152 | expect(pendingChannel.pending_channels[0].capacity).to.equal(1000000); 153 | expect(pendingChannel.pending_channels[0].partner_public_key).to.equal(lightningPublicKey2); 154 | await bitcoin.asyncCall('generatetoaddress', [6, bitcoinWalletAddress]); 155 | await new Promise(resolve => setTimeout(resolve, 1000)); 156 | const { channels } = await lightning1.asyncCall('getChannels', []); 157 | expect(channels.length).to.equal(1); 158 | expect(channels[0].is_active).to.equal(true); 159 | expect(channels[0].capacity).to.equal(1000000); 160 | expect(channels[0].partner_public_key).to.equal(lightningPublicKey2); 161 | }); 162 | 163 | it('should be able to create an invoice', async () => { 164 | const expiryDate = new Date() + 15 * 60 * 100; 165 | invoice = await lightning2.createInvoice({ id: 'lndinvoicetest', amount: 1000, expiry: expiryDate }); 166 | expect(invoice.description).to.equal('lndinvoicetest'); 167 | expect(invoice.tokens).to.equal(1000); 168 | }); 169 | 170 | it('should be able to pay an invoice and detect payment', async () => { 171 | const invoiceListener = await lightning2.asyncCall('subscribeToInvoice', [{ id: invoice.id }]); 172 | invoiceListener.on('invoice_updated', (data) => { 173 | expect(data.tokens).to.equal(1000); 174 | }); 175 | const payment = await lightning1.asyncCall('pay', [{ 176 | request: invoice.request, 177 | }]); 178 | assert(payment.confirmed_at); 179 | expect(payment.paths.length).to.equal(1); // should have used one hop over channel 180 | expect(payment.mtokens).to.equal('1000000'); // total channel capacity 181 | expect(payment.tokens).to.equal(1000); // amount invoice was paid for 182 | }); 183 | 184 | it('should be able to get past payments', async () => { 185 | await bitcoin.asyncCall('generatetoaddress', [6, bitcoinWalletAddress]); 186 | await new Promise(resolve => setTimeout(resolve, 1000)); 187 | const payments = await lightning1.asyncCall('getPayment', [{ id: invoice.id }]); 188 | expect(payments.failed).to.not.exist; 189 | expect(payments.payment.tokens).to.equal(1000); 190 | }); 191 | 192 | it('should be able to get server info', async () => { 193 | const info = await lightning1.getServerInfo({ currency }); 194 | expect(info).to.have.property('chains'); 195 | expect(info).to.have.property('color'); 196 | expect(info).to.have.property('active_channels_count'); 197 | expect(info).to.have.property('alias'); 198 | expect(info).to.have.property('current_block_hash'); 199 | expect(info).to.have.property('current_block_height'); 200 | expect(info).to.have.property('features'); 201 | expect(info).to.have.property('is_synced_to_chain'); 202 | expect(info).to.have.property('is_synced_to_graph'); 203 | expect(info).to.have.property('latest_block_at'); 204 | expect(info).to.have.property('peers_count'); 205 | expect(info).to.have.property('pending_channels_count'); 206 | expect(info).to.have.property('public_key'); 207 | expect(info).to.have.property('uris'); 208 | expect(info).to.have.property('version'); 209 | }); 210 | }); 211 | -------------------------------------------------------------------------------- /tests/ltc.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const assert = require('assert'); 3 | const mocha = require('mocha'); 4 | const sinon = require('sinon'); 5 | const { expect } = require('chai'); 6 | const { describe, it } = mocha; 7 | const config = { 8 | chain: 'LTC', 9 | host: process.env.HOST_LTC || 'litecoin', 10 | protocol: 'http', 11 | rpcPort: '10333', 12 | rpcUser: 'cryptorpc', 13 | rpcPass: 'local321', 14 | tokens: {}, 15 | currencyConfig: { 16 | sendTo: '2NGFWyW3LBPr6StDuDSNFzQF3Jouuup1rua', 17 | unlockPassword: 'password', 18 | rawTx: 19 | '0100000001641ba2d21efa8db1a08c0072663adf4c4bc3be9ee5aabb530b2d4080b8a41cca000000006a4730440220062105df71eb10b5ead104826e388303a59d5d3d134af73cdf0d5e685650f95c0220188c8a966a2d586430d84aa7624152a556550c3243baad5415c92767dcad257f0121037aaa54736c5ffa13132e8ca821be16ce4034ae79472053dde5aa4347034bc0a2ffffffff0240787d010000000017a914c8241f574dfade4d446ec90cc0e534cb120b45e387eada4f1c000000001976a9141576306b9cc227279b2a6c95c2b017bb22b0421f88ac00000000' 20 | } 21 | }; 22 | 23 | describe('LTC Tests', function() { 24 | this.timeout(10000); 25 | let txid = ''; 26 | let blockHash = ''; 27 | const currency = 'LTC'; 28 | const { currencyConfig } = config; 29 | const rpcs = new CryptoRpc(config, currencyConfig); 30 | const bitcoin = rpcs.get(currency); 31 | 32 | it('should determine if wallet is encrypted', async () => { 33 | expect(await bitcoin.isWalletEncrypted()).to.eq(false); 34 | try { 35 | await bitcoin.asyncCall('encryptWallet', ['password']); 36 | await new Promise(resolve => setTimeout(resolve, 5000)); 37 | } catch (e) { 38 | console.warn('wallet already encrypted'); 39 | } 40 | expect(await bitcoin.isWalletEncrypted()).to.eq(true); 41 | await bitcoin.asyncCall('generate', [101]); 42 | }); 43 | 44 | it('walletUnlock should unlock wallet successfully', async () => { 45 | await bitcoin.walletUnlock({ passphrase: config.currencyConfig.unlockPassword, time: 10 }); 46 | }); 47 | 48 | it('walletUnlock should error on if wrong args', async () => { 49 | await bitcoin.walletUnlock({ passphrase: config.currencyConfig.unlockPassword }) 50 | .catch(err => { 51 | assert(err); 52 | expect(typeof err).to.eq('object'); 53 | expect(err).to.have.property('message'); 54 | expect(err.message).to.eq('JSON value is not an integer as expected'); 55 | }); 56 | }); 57 | 58 | it('walletUnlock should error on if wrong passphrase', async () => { 59 | await bitcoin.walletUnlock({ passphrase: 'wrong', time: 10 }) 60 | .catch(err => { 61 | assert(err); 62 | expect(typeof err).to.eq('object'); 63 | expect(err).to.have.property('message'); 64 | expect(err.message).to.eq('Error: The wallet passphrase entered was incorrect.'); 65 | }); 66 | }); 67 | 68 | it('should be able to get a block hash', async () => { 69 | blockHash = await rpcs.getBestBlockHash({ currency }); 70 | expect(blockHash).to.have.lengthOf('64'); 71 | }); 72 | 73 | it('should convert fee to satoshis per kilobyte with estimateFee', async () => { 74 | sinon.stub(bitcoin.rpc,'estimateSmartFee').callsFake((nBlocks, cb) => { 75 | cb(null, {result: {'feerate': 0.00001234, 'blocks': 2}}); 76 | }); 77 | const fee = await bitcoin.estimateFee({nBlocks: 2}); 78 | expect(fee).to.be.eq(1.234); 79 | }); 80 | 81 | it('should get block', async () => { 82 | const reqBlock = await rpcs.getBlock({ currency, hash: blockHash }); 83 | expect(reqBlock).to.have.property('hash'); 84 | expect(reqBlock).to.have.property('confirmations'); 85 | expect(reqBlock).to.have.property('strippedsize'); 86 | expect(reqBlock).to.have.property('size'); 87 | expect(reqBlock).to.have.property('weight'); 88 | expect(reqBlock).to.have.property('height'); 89 | expect(reqBlock).to.have.property('version'); 90 | expect(reqBlock).to.have.property('versionHex'); 91 | expect(reqBlock).to.have.property('merkleroot'); 92 | expect(reqBlock).to.have.property('tx'); 93 | expect(reqBlock).to.have.property('time'); 94 | expect(reqBlock).to.have.property('mediantime'); 95 | expect(reqBlock).to.have.property('nonce'); 96 | expect(reqBlock).to.have.property('bits'); 97 | expect(reqBlock).to.have.property('difficulty'); 98 | expect(reqBlock).to.have.property('chainwork'); 99 | expect(reqBlock).to.have.property('nTx'); 100 | expect(reqBlock).to.have.property('previousblockhash'); 101 | assert(reqBlock); 102 | }); 103 | 104 | it('should be able to get a balance', async () => { 105 | const balance = await rpcs.getBalance({ currency }); 106 | expect(balance).to.eq(5000000000); 107 | assert(balance != undefined); 108 | }); 109 | 110 | it('should be able to send a transaction', async () => { 111 | txid = await rpcs.unlockAndSendToAddress({ currency, address: config.currencyConfig.sendTo, amount: '10000', passphrase: currencyConfig.unlockPassword }); 112 | expect(txid).to.have.lengthOf(64); 113 | assert(txid); 114 | }); 115 | 116 | it('should be able to send many transactions', async () => { 117 | let payToArray = []; 118 | const transaction1 = { 119 | address: 'mm7mGjBBe1sUF8SFXCW779DX8XrmpReBTg', 120 | amount: 10000 121 | }; 122 | const transaction2 = { 123 | address: 'mm7mGjBBe1sUF8SFXCW779DX8XrmpReBTg', 124 | amount: 20000 125 | }; 126 | const transaction3 = { 127 | address: 'mgoVRuvgbgyZL8iQWfS6TLPZzQnpRMHg5H', 128 | amount: 30000 129 | }; 130 | const transaction4 = { 131 | address: 'mv5XmsNbK2deMDhkVq5M28BAD14hvpQ9b2', 132 | amount: 40000 133 | }; 134 | payToArray.push(transaction1); 135 | payToArray.push(transaction2); 136 | payToArray.push(transaction3); 137 | payToArray.push(transaction4); 138 | const maxOutputs = 2; 139 | const maxValue = 1e8; 140 | const eventEmitter = rpcs.rpcs.LTC.emitter; 141 | let eventCounter = 0; 142 | let emitResults = []; 143 | const emitPromise = new Promise(resolve => { 144 | eventEmitter.on('success', (emitData) => { 145 | eventCounter++; 146 | emitResults.push(emitData); 147 | if (eventCounter === 3) { 148 | resolve(); 149 | } 150 | }); 151 | }); 152 | const outputArray = await rpcs.unlockAndSendToAddressMany({ payToArray, passphrase: currencyConfig.unlockPassword, time: 1000, maxValue, maxOutputs }); 153 | await emitPromise; 154 | expect(outputArray).to.have.lengthOf(4); 155 | expect(outputArray[0].txid).to.equal(outputArray[1].txid); 156 | expect(outputArray[2].txid).to.equal(outputArray[3].txid); 157 | expect(outputArray[1].txid).to.not.equal(outputArray[2].txid); 158 | for (let transaction of outputArray) { 159 | assert(transaction.txid); 160 | expect(transaction.txid).to.have.lengthOf(64); 161 | } 162 | for (let emitData of emitResults) { 163 | assert(emitData.address); 164 | assert(emitData.amount); 165 | assert(emitData.txid); 166 | expect(emitData.error === null); 167 | expect(emitData.vout === 0 || emitData.vout === 1); 168 | let transactionObj = {address: emitData.address, amount: emitData.amount}; 169 | expect(payToArray.includes(transactionObj)); 170 | } 171 | }); 172 | 173 | it('should reject when one of many transactions fails', async () => { 174 | const address = config.currencyConfig.sendTo; 175 | const amount = '1000'; 176 | const payToArray = [ 177 | { address, amount }, 178 | { address: 'funkyColdMedina', amount: 1 } 179 | ]; 180 | const eventEmitter = rpcs.rpcs.LTC.emitter; 181 | let emitResults = []; 182 | const emitPromise = new Promise(resolve => { 183 | eventEmitter.on('failure', (emitData) => { 184 | emitResults.push(emitData); 185 | resolve(); 186 | }); 187 | }); 188 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 189 | currency, 190 | payToArray, 191 | passphrase: currencyConfig.unlockPassword 192 | }); 193 | await emitPromise; 194 | assert(!outputArray[1].txid); 195 | expect(outputArray[1].error).to.equal(emitResults[0].error); 196 | expect(emitResults.length).to.equal(1); 197 | assert(emitResults[0].error); 198 | }); 199 | 200 | it('should be able to get a transaction', async () => { 201 | const tx = await rpcs.getTransaction({ currency, txid }); 202 | expect(tx).to.have.property('txid'); 203 | expect(tx).to.have.property('hash'); 204 | expect(tx).to.have.property('version'); 205 | expect(tx).to.have.property('size'); 206 | expect(tx).to.have.property('vsize'); 207 | expect(tx).to.have.property('locktime'); 208 | expect(tx).to.have.property('vin'); 209 | expect(tx).to.have.property('vout'); 210 | expect(tx).to.have.property('hex'); 211 | assert(tx); 212 | assert(typeof tx === 'object'); 213 | }); 214 | 215 | it('should be able to decode a raw transaction', async () => { 216 | const { rawTx } = config.currencyConfig; 217 | assert(rawTx); 218 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 219 | expect(decoded).to.have.property('txid'); 220 | expect(decoded).to.have.property('hash'); 221 | expect(decoded).to.have.property('version'); 222 | expect(decoded).to.have.property('size'); 223 | expect(decoded).to.have.property('vsize'); 224 | expect(decoded).to.have.property('locktime'); 225 | expect(decoded).to.have.property('vin'); 226 | expect(decoded).to.have.property('vout'); 227 | assert(decoded); 228 | }); 229 | 230 | it('should get the tip', async () => { 231 | const tip = await rpcs.getTip({ currency }); 232 | assert(tip != undefined); 233 | expect(tip).to.have.property('hash'); 234 | expect(tip).to.have.property('height'); 235 | }); 236 | 237 | it('should get confirmations', async () => { 238 | let confirmations = await rpcs.getConfirmations({ currency, txid }); 239 | assert(confirmations != undefined); 240 | expect(confirmations).to.eq(0); 241 | await bitcoin.asyncCall('generate', [1]); 242 | confirmations = await rpcs.getConfirmations({ currency, txid }); 243 | expect(confirmations).to.eq(1); 244 | }); 245 | 246 | it('should validate address', async () => { 247 | const isValid = await rpcs.validateAddress({ currency, address: config.currencyConfig.sendTo }); 248 | assert(isValid === true); 249 | }); 250 | 251 | it('should not validate bad address', async () => { 252 | const isValid = await rpcs.validateAddress({ currency, address: 'NOTANADDRESS' }); 253 | assert(isValid === false); 254 | }); 255 | 256 | it('should be able to send a batched transaction', async() => { 257 | let address1 = 'mtXWDB6k5yC5v7TcwKZHB89SUp85yCKshy'; 258 | let amount1 = '10000'; 259 | let address2 = 'msngvArStqsSqmkG7W7Fc9jotPcURyLyYu'; 260 | let amount2 = '20000'; 261 | const batch = {}; 262 | batch[address1] = amount1; 263 | batch[address2] = amount2; 264 | 265 | await bitcoin.walletUnlock({ passphrase: config.currencyConfig.unlockPassword, time: 10 }); 266 | let txid = await bitcoin.sendMany({ batch, options: null }); 267 | await bitcoin.walletLock(); 268 | expect(txid).to.have.lengthOf(64); 269 | assert(txid); 270 | }); 271 | 272 | it('should be able to get server info', async () => { 273 | const info = await rpcs.getServerInfo({ currency }); 274 | expect(info).to.have.property('chain'); 275 | expect(info).to.have.property('blocks'); 276 | expect(info).to.have.property('headers'); 277 | expect(info).to.have.property('bestblockhash'); 278 | expect(info).to.have.property('difficulty'); 279 | // expect(info).to.have.property('time'); 280 | expect(info).to.have.property('mediantime'); 281 | expect(info).to.have.property('verificationprogress'); 282 | expect(info).to.have.property('initialblockdownload'); 283 | expect(info).to.have.property('chainwork'); 284 | expect(info).to.have.property('size_on_disk'); 285 | expect(info).to.have.property('pruned'); 286 | expect(info).to.have.property('warnings'); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /tests/xrp.js: -------------------------------------------------------------------------------- 1 | const { CryptoRpc } = require('../'); 2 | const {assert, expect} = require('chai'); 3 | const mocha = require('mocha'); 4 | const {describe, it, before} = mocha; 5 | const config = { 6 | chain: 'XRP', 7 | currency: 'XRP', 8 | host: process.env.HOST_XRP || 'rippled', 9 | protocol: 'ws', 10 | rpcPort: '6006', 11 | address: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', 12 | currencyConfig: { 13 | sendTo: 'rDFrG4CgPFMnQFJBmZH7oqTjLuiB3HS4eu', 14 | privateKey: 15 | '117ACF0C71DE079057F4D125948D2F1F12CB3F47C234E43438E1E44C93A9C583', 16 | rawTx: 17 | '12000322800000002400000017201B0086955368400000000000000C732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D874473045022100BDE09A1F6670403F341C21A77CF35BA47E45CDE974096E1AA5FC39811D8269E702203D60291B9A27F1DCABA9CF5DED307B4F23223E0B6F156991DB601DFB9C41CE1C770A726970706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304' 18 | }, 19 | connectionIdleMs: 250 20 | }; 21 | 22 | describe('XRP Tests', function() { 23 | let currency = 'XRP'; 24 | let blockHash = ''; 25 | let block; 26 | let txid = ''; 27 | 28 | let rpcs; 29 | let xrpRPC; 30 | 31 | before(() => { 32 | rpcs = new CryptoRpc(config); 33 | xrpRPC = rpcs.get(currency); 34 | }); 35 | 36 | it('should be able to get best block hash', async () => { 37 | try { 38 | blockHash = await rpcs.getBestBlockHash({ currency }); 39 | } catch (err) { 40 | expect(err).to.not.exist(); 41 | } 42 | 43 | expect(blockHash).to.have.lengthOf('64'); 44 | }); 45 | 46 | it('should estimate fee', async () => { 47 | let fee; 48 | try { 49 | fee = await xrpRPC.estimateFee(); 50 | } catch (err) { 51 | expect(err).to.not.exist(); 52 | } 53 | assert.isTrue(fee === '10'); 54 | }); 55 | 56 | const blockCases = [ 57 | { description: 'by hash', params: { hash: blockHash } }, 58 | { description: 'by index', params: { index: 'defined below' } }, 59 | { description: 'by latest', params: { index: 'latest' } }, 60 | ]; 61 | Object.defineProperty(blockCases[1].params, 'index', { get: () => block.ledger_index }); 62 | 63 | for (const bcase of blockCases) { 64 | it(`should get block ${bcase.description}`, async () => { 65 | try { 66 | block = await rpcs.getBlock({ currency, ...bcase.params }); 67 | } catch (err) { 68 | expect(err).to.not.exist(); 69 | } 70 | 71 | expect(block).to.have.property('ledger'); 72 | let ledger = block.ledger; 73 | // from xrpl documentation: https://xrpl.org/ledger.html (9/26/2023) 74 | // The following fields are deprecated and may be removed without further notice: accepted, totalCoins (use total_coins instead). 75 | // as a result the following is commented out 76 | // expect(ledger).to.have.property('accepted'); 77 | // expect(ledger.accepted).to.equal(true); 78 | expect(ledger).to.have.property('ledger_hash'); 79 | expect(ledger).to.have.property('ledger_index'); 80 | expect(ledger).to.have.property('parent_hash'); 81 | expect(ledger).to.have.property('transactions'); 82 | expect(ledger.transactions).to.deep.equal([]); 83 | expect(block).to.have.property('ledger_hash'); 84 | expect(block).to.have.property('ledger_index'); 85 | expect(block.ledger_hash).to.equal(ledger.ledger_hash); 86 | expect(block.ledger_index.toString()).to.equal(ledger.ledger_index); 87 | expect(block).to.have.property('validated'); 88 | expect(block.validated).to.equal(true); 89 | assert(block); 90 | }); 91 | } 92 | 93 | it('should return nothing for unknown block', async () => { 94 | let unknownBlock; 95 | try { 96 | unknownBlock = await rpcs.getBlock({ currency, hash: '1723099E269C77C4BDE86C83FA6415D71CF20AA5CB4A94E5C388ED97123FB55B' }); 97 | } catch (err) { 98 | expect(err).to.not.exist(); 99 | } 100 | expect(unknownBlock).to.be.null; 101 | }); 102 | 103 | it('should be able to get a balance', async () => { 104 | let balance; 105 | try { 106 | balance = await rpcs.getBalance({ currency, address: config.address }); 107 | } catch (err) { 108 | expect(err).to.not.exist(); 109 | } 110 | expect(balance).to.eq(100000000000); 111 | assert(balance != undefined); 112 | }); 113 | 114 | it('should be able to send a transaction', async () => { 115 | await xrpRPC.asyncRequest('ledger_accept'); 116 | let beforeToBalance; 117 | try { 118 | beforeToBalance = await rpcs.getBalance({ currency, address: config.currencyConfig.sendTo }); 119 | } catch (err) { 120 | beforeToBalance = 0; 121 | } 122 | try { 123 | txid = await rpcs.unlockAndSendToAddress({ currency, address: config.currencyConfig.sendTo, amount: '10000', secret: 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' }); 124 | } catch (err) { 125 | expect(err).to.not.exist(); 126 | } 127 | 128 | expect(txid).to.have.lengthOf(64); 129 | assert(txid); 130 | await xrpRPC.asyncRequest('ledger_accept'); 131 | let afterToBalance = await rpcs.getBalance({ currency, address: config.currencyConfig.sendTo }); 132 | expect(afterToBalance - beforeToBalance).to.eq(10000); 133 | }); 134 | 135 | 136 | it('should be able to send many transactions', async () => { 137 | let payToArray = []; 138 | const transaction1 = { 139 | address: 'r38UsJxHSJKajC8qcNmofxJvCESnzmx7Ke', 140 | amount: 10000 141 | }; 142 | const transaction2 = { 143 | address: 'rMGhv5SNsk81QN1fGu6RybDkUi2of36dua', 144 | amount: 20000 145 | }; 146 | const transaction3 = { 147 | address: 'r4ip6t3NUe4UWguLUJCbyojxG6PdPZg9EJ', 148 | amount: 30000 149 | }; 150 | const transaction4 = { 151 | address: 'rwtFtAMNXPoq4xgxn3FzKKGgVZErdcuLST', 152 | amount: 40000 153 | }; 154 | payToArray.push(transaction1); 155 | payToArray.push(transaction2); 156 | payToArray.push(transaction3); 157 | payToArray.push(transaction4); 158 | const eventEmitter = rpcs.rpcs.XRP.emitter; 159 | let eventCounter = 0; 160 | let emitResults = []; 161 | const emitPromise = new Promise(resolve => { 162 | eventEmitter.on('success', (emitData) => { 163 | eventCounter++; 164 | emitResults.push(emitData); 165 | if (eventCounter === 3) { 166 | resolve(); 167 | } 168 | }); 169 | }); 170 | const outputArray = await rpcs.unlockAndSendToAddressMany({ payToArray, secret: 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' }); 171 | await emitPromise; 172 | expect(outputArray).to.have.lengthOf(4); 173 | expect(outputArray[0]).to.have.property('txid'); 174 | expect(outputArray[1]).to.have.property('txid'); 175 | expect(outputArray[2]).to.have.property('txid'); 176 | expect(outputArray[3]).to.have.property('txid'); 177 | for (let transaction of outputArray) { 178 | assert(transaction.txid); 179 | expect(transaction.txid).to.have.lengthOf(64); 180 | } 181 | for (let emitData of emitResults) { 182 | assert(emitData.address); 183 | assert(emitData.amount); 184 | assert(emitData.txid); 185 | expect(emitData.error === null); 186 | expect(emitData.vout === 0 || emitData.vout === 1); 187 | let transactionObj = {address: emitData.address, amount: emitData.amount}; 188 | expect(payToArray.includes(transactionObj)); 189 | } 190 | }); 191 | 192 | it('should reject when one of many transactions fails', async () => { 193 | const address = config.currencyConfig.sendTo; 194 | const amount = '1000'; 195 | const payToArray = [ 196 | { address, amount }, 197 | { address: 'funkyColdMedina', amount: 1 } 198 | ]; 199 | const eventEmitter = rpcs.rpcs.XRP.emitter; 200 | let emitResults = []; 201 | const emitPromise = new Promise(resolve => { 202 | eventEmitter.on('failure', (emitData) => { 203 | emitResults.push(emitData); 204 | resolve(); 205 | }); 206 | }); 207 | const outputArray = await rpcs.unlockAndSendToAddressMany({ 208 | currency, 209 | payToArray, 210 | secret: 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' 211 | }); 212 | await emitPromise; 213 | expect(emitResults.length).to.equal(1); 214 | assert(emitResults[0].error); 215 | assert(!outputArray[1].txid); 216 | expect(outputArray[1].error).to.equal(emitResults[0].error); 217 | }); 218 | 219 | it('should be able to get a transaction', async () => { 220 | let tx; 221 | try { 222 | tx = await rpcs.getTransaction({ currency, txid }); 223 | } catch (err) { 224 | expect(err).to.not.exist(); 225 | } 226 | expect(tx).to.have.property('Account'); 227 | expect(tx).to.have.property('Amount'); 228 | expect(tx).to.have.property('Destination'); 229 | expect(tx).to.have.property('Fee'); 230 | expect(tx).to.have.property('Flags'); 231 | expect(tx).to.have.property('LastLedgerSequence'); 232 | expect(tx).to.have.property('Sequence'); 233 | expect(tx).to.have.property('hash'); 234 | expect(tx.hash).to.equal(txid); 235 | expect(tx).to.have.property('blockHash'); 236 | expect(tx.blockHash).to.not.be.undefined; 237 | }); 238 | 239 | it('should return nothing for unknown transaction', async () => { 240 | let unknownTx; 241 | try { 242 | unknownTx = await rpcs.getTransaction({ currency, txid }); 243 | } catch (err) { 244 | expect(err).to.not.exist(); 245 | } 246 | expect(unknownTx === null); 247 | }); 248 | 249 | it('should be able to get a raw transaction', async () => { 250 | let tx; 251 | try { 252 | tx = await rpcs.getRawTransaction({ currency, txid }); 253 | } catch (err) { 254 | expect(err).to.not.exist(); 255 | } 256 | expect(tx.length).to.be.greaterThan(300); 257 | }); 258 | 259 | it('should return nothing for unknown raw transaction', async () => { 260 | let tx; 261 | try { 262 | tx = await rpcs.getRawTransaction({ currency, txid: 'E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7' }); 263 | } catch (err) { 264 | expect(err).to.not.exist(); 265 | } 266 | expect(tx === null); 267 | }); 268 | 269 | it('should be able to decode a raw transaction', async () => { 270 | const { rawTx } = config.currencyConfig; 271 | assert(rawTx); 272 | const decoded = await rpcs.decodeRawTransaction({ currency, rawTx }); 273 | expect(decoded).to.have.property('Fee'); 274 | expect(decoded).to.have.property('Sequence'); 275 | expect(decoded).to.have.property('Account'); 276 | expect(decoded).to.have.property('TxnSignature'); 277 | expect(decoded).to.have.property('SigningPubKey'); 278 | expect(decoded).to.have.property('Sequence'); 279 | expect(decoded).to.have.property('TransactionType'); 280 | expect(decoded.TransactionType).to.deep.equal('AccountSet'); 281 | assert(decoded); 282 | }); 283 | 284 | it('should get the tip', async () => { 285 | const tip = await rpcs.getTip({ currency }); 286 | assert(tip != undefined); 287 | expect(tip).to.have.property('hash'); 288 | expect(tip).to.have.property('height'); 289 | }); 290 | 291 | it('should get confirmations', async () => { 292 | let confirmationsBefore = await rpcs.getConfirmations({ currency, txid }); 293 | assert(confirmationsBefore != undefined); 294 | let { result:acceptance} = await xrpRPC.asyncRequest('ledger_accept'); 295 | assert(acceptance); 296 | expect(acceptance).to.have.property('ledger_current_index'); 297 | let confirmationsAfter = await rpcs.getConfirmations({ currency, txid }); 298 | expect(confirmationsAfter - confirmationsBefore).to.eq(1); 299 | }); 300 | 301 | it('should not return confirmations for unknown transaction', async () => { 302 | let confirmations = await rpcs.getConfirmations({ currency, txid }); 303 | expect(confirmations === null); 304 | }); 305 | 306 | it('should validate address', async () => { 307 | const isValid = await rpcs.validateAddress({ currency, address: config.currencyConfig.sendTo }); 308 | assert(isValid === true); 309 | }); 310 | 311 | it('should not validate bad address', async () => { 312 | const isValid = await rpcs.validateAddress({ currency, address: 'NOTANADDRESS' }); 313 | assert(isValid === false); 314 | }); 315 | 316 | it('should get account info', async () => { 317 | const accountInfo = await rpcs.getAccountInfo({ currency, address: config.address }); 318 | expect(accountInfo).to.have.property('account_data'); 319 | expect(accountInfo.account_data).to.have.property('Balance'); 320 | expect(accountInfo.account_data).to.have.property('Flags'); 321 | expect(accountInfo.account_data).to.have.property('index'); 322 | expect(accountInfo.account_data).to.have.property('LedgerEntryType'); 323 | expect(accountInfo.account_data).to.have.property('OwnerCount'); 324 | expect(accountInfo.account_data).to.have.property('PreviousTxnID'); 325 | expect(accountInfo.account_data).to.have.property('PreviousTxnLgrSeq'); 326 | expect(accountInfo.account_data).to.have.property('Sequence'); 327 | }); 328 | 329 | it('should get server info', async () => { 330 | const serverInfo = await rpcs.getServerInfo({ currency }); 331 | expect(serverInfo).to.have.property('complete_ledgers'); 332 | expect(serverInfo).to.have.property('server_state'); 333 | expect(serverInfo).to.have.property('uptime'); 334 | expect(serverInfo).to.have.property('validated_ledger'); 335 | expect(serverInfo.validated_ledger).to.have.property('reserve_base_xrp'); 336 | }); 337 | 338 | it('should disconnect from rpc when idle', async () => { 339 | await rpcs.getTip({ currency }); 340 | assert(xrpRPC.rpc.isConnected() === true, 'connection should be connected'); 341 | await new Promise((resolve) => setTimeout(resolve, 300)); 342 | assert(xrpRPC.rpc.isConnected() === false, 'connection should be disconnected'); 343 | }); 344 | 345 | it('should handle emitted connection errors from rpc with noop', async () => { 346 | xrpRPC.rpc.emit('error', new Error('connection error xrp')); 347 | }); 348 | }); 349 | --------------------------------------------------------------------------------