diff --git a/packages/bitcore-node/src/config.ts b/packages/bitcore-node/src/config.ts index e1ae80d0a78..83bb164182d 100644 --- a/packages/bitcore-node/src/config.ts +++ b/packages/bitcore-node/src/config.ts @@ -62,7 +62,7 @@ const Config = function(): ConfigType { dbPass: process.env.DB_PASS || '', numWorkers: cpus().length, chains: {}, - modules: ['./bitcoin', './bitcoin-cash', './ethereum'], + modules: ['./bitcoin', './bitcoin-cash', './ethereum', './rsk'], services: { api: { rateLimiter: { diff --git a/packages/bitcore-node/src/modules/index.ts b/packages/bitcore-node/src/modules/index.ts index 1903b1fbfe3..78633f7f0f2 100644 --- a/packages/bitcore-node/src/modules/index.ts +++ b/packages/bitcore-node/src/modules/index.ts @@ -53,7 +53,8 @@ class ModuleManager extends BaseModule { BCH: './bitcoin-cash', DOGE: './dogecoin', LTC: './litecoin', - XRP: './ripple' + XRP: './ripple', + RSK: './rsk' }; loadConfigured() { diff --git a/packages/bitcore-node/src/modules/modules.md b/packages/bitcore-node/src/modules/modules.md index b5a03d1f88c..784726d4452 100644 --- a/packages/bitcore-node/src/modules/modules.md +++ b/packages/bitcore-node/src/modules/modules.md @@ -12,6 +12,7 @@ The modules in this table will automatically register with `bitcore-node` if you | LTC | litecoin | ./litecoin | | DOGE | dogecoin | ./dogecoin | | XRP | ripple | ./ripple | +| RSK | rsk | ./rsk | If there is a custom or third-party module you'd like to use, follow the example below. diff --git a/packages/bitcore-node/src/modules/rsk/abi/erc20.ts b/packages/bitcore-node/src/modules/rsk/abi/erc20.ts new file mode 100644 index 00000000000..dbe32a59f99 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/abi/erc20.ts @@ -0,0 +1,272 @@ +export const ERC20Abi = [ + { + constant: true, + inputs: [], + name: 'name', + outputs: [ + { + name: '', + type: 'string' + } + ], + payable: false, + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'approve', + outputs: [ + { + name: 'success', + type: 'bool' + } + ], + payable: false, + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: '_from', + type: 'address' + }, + { + name: '_to', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'transferFrom', + outputs: [ + { + name: 'success', + type: 'bool' + } + ], + payable: false, + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'decimals', + outputs: [ + { + name: '', + type: 'uint8' + } + ], + payable: false, + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'version', + outputs: [ + { + name: '', + type: 'string' + } + ], + payable: false, + type: 'function' + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + name: 'balance', + type: 'uint256' + } + ], + payable: false, + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'symbol', + outputs: [ + { + name: '', + type: 'string' + } + ], + payable: false, + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: '_to', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'transfer', + outputs: [ + { + name: 'success', + type: 'bool' + } + ], + payable: false, + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + }, + { + name: '_extraData', + type: 'bytes' + } + ], + name: 'approveAndCall', + outputs: [ + { + name: 'success', + type: 'bool' + } + ], + payable: false, + type: 'function' + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address' + }, + { + name: '_spender', + type: 'address' + } + ], + name: 'allowance', + outputs: [ + { + name: 'remaining', + type: 'uint256' + } + ], + payable: false, + type: 'function' + }, + { + inputs: [ + { + name: '_initialAmount', + type: 'uint256' + }, + { + name: '_tokenName', + type: 'string' + }, + { + name: '_decimalUnits', + type: 'uint8' + }, + { + name: '_tokenSymbol', + type: 'string' + } + ], + type: 'constructor' + }, + { + payable: false, + type: 'fallback' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: '_from', + type: 'address' + }, + { + indexed: true, + name: '_to', + type: 'address' + }, + { + indexed: false, + name: '_value', + type: 'uint256' + } + ], + name: 'Transfer', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: '_owner', + type: 'address' + }, + { + indexed: true, + name: '_spender', + type: 'address' + }, + { + indexed: false, + name: '_value', + type: 'uint256' + } + ], + name: 'Approval', + type: 'event' + } +]; diff --git a/packages/bitcore-node/src/modules/rsk/abi/erc721.ts b/packages/bitcore-node/src/modules/rsk/abi/erc721.ts new file mode 100644 index 00000000000..c023756505e --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/abi/erc721.ts @@ -0,0 +1,241 @@ +export const ERC721Abi = [ + { + constant: true, + inputs: [{ name: '_interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ name: '', type: 'bool' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'name', + outputs: [{ name: '', type: 'string' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [{ name: '_tokenId', type: 'uint256' }], + name: 'getApproved', + outputs: [{ name: '', type: 'address' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { name: '_to', type: 'address' }, + { name: '_tokenId', type: 'uint256' } + ], + name: 'approve', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'InterfaceId_ERC165', + outputs: [{ name: '', type: 'bytes4' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { name: '_from', type: 'address' }, + { name: '_to', type: 'address' }, + { name: '_tokenId', type: 'uint256' } + ], + name: 'transferFrom', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: true, + inputs: [ + { name: '_owner', type: 'address' }, + { name: '_index', type: 'uint256' } + ], + name: 'tokenOfOwnerByIndex', + outputs: [{ name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { name: '_from', type: 'address' }, + { name: '_to', type: 'address' }, + { name: '_tokenId', type: 'uint256' } + ], + name: 'safeTransferFrom', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: true, + inputs: [{ name: '_tokenId', type: 'uint256' }], + name: 'exists', + outputs: [{ name: '', type: 'bool' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [{ name: '_index', type: 'uint256' }], + name: 'tokenByIndex', + outputs: [{ name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [{ name: '_tokenId', type: 'uint256' }], + name: 'ownerOf', + outputs: [{ name: '', type: 'address' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [{ name: '_owner', type: 'address' }], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'symbol', + outputs: [{ name: '', type: 'string' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { name: '_to', type: 'address' }, + { name: '_approved', type: 'bool' } + ], + name: 'setApprovalForAll', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: false, + inputs: [ + { name: '_from', type: 'address' }, + { name: '_to', type: 'address' }, + { name: '_tokenId', type: 'uint256' }, + { name: '_data', type: 'bytes' } + ], + name: 'safeTransferFrom', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: true, + inputs: [{ name: '_tokenId', type: 'uint256' }], + name: 'tokenURI', + outputs: [{ name: '', type: 'string' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { name: '_owner', type: 'address' }, + { name: '_operator', type: 'address' } + ], + name: 'isApprovedForAll', + outputs: [{ name: '', type: 'bool' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { name: '_name', type: 'string' }, + { name: '_symbol', type: 'string' } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'constructor' + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: '_from', type: 'address' }, + { indexed: true, name: '_to', type: 'address' }, + { indexed: true, name: '_tokenId', type: 'uint256' } + ], + name: 'Transfer', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: '_owner', type: 'address' }, + { indexed: true, name: '_approved', type: 'address' }, + { indexed: true, name: '_tokenId', type: 'uint256' } + ], + name: 'Approval', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: '_owner', type: 'address' }, + { indexed: true, name: '_operator', type: 'address' }, + { indexed: false, name: '_approved', type: 'bool' } + ], + name: 'ApprovalForAll', + type: 'event' + }, + { + constant: false, + inputs: [ + { name: '_to', type: 'address' }, + { name: '_tokenId', type: 'uint256' }, + { name: '_tokenURI', type: 'string' } + ], + name: 'mintUniqueTokenTo', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + } +]; diff --git a/packages/bitcore-node/src/modules/rsk/abi/invoice.ts b/packages/bitcore-node/src/modules/rsk/abi/invoice.ts new file mode 100644 index 00000000000..34040f619a6 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/abi/invoice.ts @@ -0,0 +1,277 @@ +export const InvoiceAbi = [ + { + constant: true, + inputs: [], + name: 'owner', + outputs: [ + { + name: '', + type: 'address' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'quoteSigner', + outputs: [ + { + name: '', + type: 'address' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + name: '', + type: 'bytes32' + } + ], + name: 'isPaid', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + name: 'valueSigner', + type: 'address' + } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'constructor' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'hash', + type: 'bytes32' + }, + { + indexed: true, + name: 'tokenContract', + type: 'address' + }, + { + indexed: false, + name: 'time', + type: 'uint256' + }, + { + indexed: false, + name: 'value', + type: 'uint256' + } + ], + name: 'PaymentAccepted', + type: 'event' + }, + { + constant: true, + inputs: [ + { + name: 'value', + type: 'uint256' + }, + { + name: 'gasPrice', + type: 'uint256' + }, + { + name: 'expiration', + type: 'uint256' + }, + { + name: 'payload', + type: 'bytes32' + }, + { + name: 'hash', + type: 'bytes32' + }, + { + name: 'v', + type: 'uint8' + }, + { + name: 'r', + type: 'bytes32' + }, + { + name: 's', + type: 'bytes32' + }, + { + name: 'tokenContract', + type: 'address' + } + ], + name: 'isValidPayment', + outputs: [ + { + name: 'valid', + type: 'bool' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + name: 'value', + type: 'uint256' + }, + { + name: 'gasPrice', + type: 'uint256' + }, + { + name: 'expiration', + type: 'uint256' + }, + { + name: 'payload', + type: 'bytes32' + }, + { + name: 'hash', + type: 'bytes32' + }, + { + name: 'v', + type: 'uint8' + }, + { + name: 'r', + type: 'bytes32' + }, + { + name: 's', + type: 'bytes32' + }, + { + name: 'tokenContract', + type: 'address' + } + ], + name: 'validatePayment', + outputs: [ + { + name: 'valid', + type: 'bool' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: 'value', + type: 'uint256' + }, + { + name: 'gasPrice', + type: 'uint256' + }, + { + name: 'expiration', + type: 'uint256' + }, + { + name: 'payload', + type: 'bytes32' + }, + { + name: 'hash', + type: 'bytes32' + }, + { + name: 'v', + type: 'uint8' + }, + { + name: 'r', + type: 'bytes32' + }, + { + name: 's', + type: 'bytes32' + }, + { + name: 'tokenContract', + type: 'address' + } + ], + name: 'pay', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: 'tokenContract', + type: 'address' + } + ], + name: 'withdraw', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: 'newQuoteSigner', + type: 'address' + } + ], + name: 'setSigner', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: 'newAdmin', + type: 'address' + } + ], + name: 'setAdmin', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + } +]; diff --git a/packages/bitcore-node/src/modules/rsk/abi/multisig.ts b/packages/bitcore-node/src/modules/rsk/abi/multisig.ts new file mode 100644 index 00000000000..1ca8347bac6 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/abi/multisig.ts @@ -0,0 +1,634 @@ +export const MultisigAbi = [ + { + constant: true, + inputs: [ + { + name: '', + type: 'uint256' + } + ], + name: 'owners', + outputs: [ + { + name: '', + type: 'address' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: false, + inputs: [ + { + name: 'owner', + type: 'address' + } + ], + name: 'removeOwner', + outputs: [], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + constant: false, + inputs: [ + { + name: 'transactionId', + type: 'uint256' + } + ], + name: 'revokeConfirmation', + outputs: [], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + constant: true, + inputs: [ + { + name: '', + type: 'address' + } + ], + name: 'isOwner', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [ + { + name: '', + type: 'uint256' + }, + { + name: '', + type: 'address' + } + ], + name: 'confirmations', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [ + { + name: 'pending', + type: 'bool' + }, + { + name: 'executed', + type: 'bool' + } + ], + name: 'getTransactionCount', + outputs: [ + { + name: 'count', + type: 'uint256' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: false, + inputs: [ + { + name: 'owner', + type: 'address' + } + ], + name: 'addOwner', + outputs: [], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + constant: true, + inputs: [ + { + name: 'transactionId', + type: 'uint256' + } + ], + name: 'isConfirmed', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [ + { + name: 'transactionId', + type: 'uint256' + } + ], + name: 'getConfirmationCount', + outputs: [ + { + name: 'count', + type: 'uint256' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [ + { + name: '', + type: 'uint256' + } + ], + name: 'transactions', + outputs: [ + { + name: 'destination', + type: 'address' + }, + { + name: 'value', + type: 'uint256' + }, + { + name: 'data', + type: 'bytes' + }, + { + name: 'executed', + type: 'bool' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [], + name: 'getOwners', + outputs: [ + { + name: '', + type: 'address[]' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [ + { + name: 'from', + type: 'uint256' + }, + { + name: 'to', + type: 'uint256' + }, + { + name: 'pending', + type: 'bool' + }, + { + name: 'executed', + type: 'bool' + } + ], + name: 'getTransactionIds', + outputs: [ + { + name: '_transactionIds', + type: 'uint256[]' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [ + { + name: 'transactionId', + type: 'uint256' + } + ], + name: 'getConfirmations', + outputs: [ + { + name: '_confirmations', + type: 'address[]' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [], + name: 'transactionCount', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: false, + inputs: [ + { + name: '_required', + type: 'uint256' + } + ], + name: 'changeRequirement', + outputs: [], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + constant: false, + inputs: [ + { + name: 'transactionId', + type: 'uint256' + } + ], + name: 'confirmTransaction', + outputs: [], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + constant: false, + inputs: [ + { + name: 'destination', + type: 'address' + }, + { + name: 'value', + type: 'uint256' + }, + { + name: 'data', + type: 'bytes' + } + ], + name: 'submitTransaction', + outputs: [ + { + name: 'transactionId', + type: 'uint256' + } + ], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + constant: true, + inputs: [], + name: 'MAX_OWNER_COUNT', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [], + name: 'required', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: false, + inputs: [ + { + name: 'owner', + type: 'address' + }, + { + name: 'newOwner', + type: 'address' + } + ], + name: 'replaceOwner', + outputs: [], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + constant: false, + inputs: [ + { + name: 'transactionId', + type: 'uint256' + } + ], + name: 'executeTransaction', + outputs: [], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + inputs: [ + { + name: '_owners', + type: 'address[]' + }, + { + name: '_required', + type: 'uint256' + } + ], + payable: false, + type: 'constructor', + stateMutability: 'nonpayable' + }, + { + payable: true, + type: 'fallback', + stateMutability: 'payable' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'sender', + type: 'address' + }, + { + indexed: true, + name: 'transactionId', + type: 'uint256' + } + ], + name: 'Confirmation', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'sender', + type: 'address' + }, + { + indexed: true, + name: 'transactionId', + type: 'uint256' + } + ], + name: 'Revocation', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'transactionId', + type: 'uint256' + } + ], + name: 'Submission', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'transactionId', + type: 'uint256' + } + ], + name: 'Execution', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'transactionId', + type: 'uint256' + } + ], + name: 'ExecutionFailure', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'sender', + type: 'address' + }, + { + indexed: false, + name: 'value', + type: 'uint256' + } + ], + name: 'Deposit', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'owner', + type: 'address' + } + ], + name: 'OwnerAddition', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'owner', + type: 'address' + } + ], + name: 'OwnerRemoval', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + name: 'required', + type: 'uint256' + } + ], + name: 'RequirementChange', + type: 'event' + }, + { + constant: true, + inputs: [ + { + name: '', + type: 'address' + } + ], + name: 'isInstantiation', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [ + { + name: '', + type: 'address' + }, + { + name: '', + type: 'uint256' + } + ], + name: 'instantiations', + outputs: [ + { + name: '', + type: 'address' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: true, + inputs: [ + { + name: 'creator', + type: 'address' + } + ], + name: 'getInstantiationCount', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + type: 'function', + stateMutability: 'view' + }, + { + constant: false, + inputs: [ + { + name: '_owners', + type: 'address[]' + }, + { + name: '_required', + type: 'uint256' + } + ], + name: 'create', + outputs: [ + { + name: 'wallet', + type: 'address' + } + ], + payable: false, + type: 'function', + stateMutability: 'nonpayable' + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + name: 'sender', + type: 'address' + }, + { + indexed: false, + name: 'instantiation', + type: 'address' + } + ], + name: 'ContractInstantiation', + type: 'event' + } +]; diff --git a/packages/bitcore-node/src/modules/rsk/api/csp.ts b/packages/bitcore-node/src/modules/rsk/api/csp.ts new file mode 100644 index 00000000000..43364978079 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/api/csp.ts @@ -0,0 +1,579 @@ +import { CryptoRpc } from 'crypto-rpc'; +import { ObjectID } from 'mongodb'; +import { Readable } from 'stream'; +import Web3 from 'web3'; +import { Transaction } from 'web3-eth'; +import { AbiItem } from 'web3-utils'; +import Config from '../../../config'; +import logger from '../../../logger'; +import { MongoBound } from '../../../models/base'; +import { ITransaction } from '../../../models/baseTransaction'; +import { CacheStorage } from '../../../models/cache'; +import { WalletAddressStorage } from '../../../models/walletAddress'; +import { InternalStateProvider } from '../../../providers/chain-state/internal/internal'; +import { Storage } from '../../../services/storage'; +import { SpentHeightIndicators } from '../../../types/Coin'; +import { + BroadcastTransactionParams, + GetBalanceForAddressParams, + GetBlockParams, + GetWalletBalanceParams, + IChainStateService, + StreamAddressUtxosParams, + StreamTransactionParams, + StreamTransactionsParams, + StreamWalletTransactionsArgs, + StreamWalletTransactionsParams, + UpdateWalletParams +} from '../../../types/namespaces/ChainStateProvider'; +import { partition } from '../../../utils/partition'; +import { StatsUtil } from '../../../utils/stats'; +import { ERC20Abi } from '../abi/erc20'; +import { RskBlockStorage } from '../models/block'; +import { RskTransactionStorage } from '../models/transaction'; +import { IRskBlock, IRskTransaction, RskTransactionJSON } from '../types'; +import { Erc20RelatedFilterTransform } from './erc20Transform'; +import { InternalTxRelatedFilterTransform } from './internalTxTransform'; +import { PopulateReceiptTransform } from './populateReceiptTransform'; +import { RskListTransactionsStream } from './transform'; +export interface EventLog { + event: string; + address: string; + returnValues: T; + logIndex: number; + transactionIndex: number; + transactionHash: string; + blockHash: string; + blockNumber: number; + raw?: { data: string; topics: any[] }; +} +interface ERC20Transfer + extends EventLog<{ + [key: string]: string; + }> {} + +export class RSKStateProvider extends InternalStateProvider implements IChainStateService { + config: any; + static rpcs = {} as { [network: string]: { rpc: CryptoRpc; web3: Web3 } }; + + constructor(public chain: string = 'RSK') { + super(chain); + this.config = Config.chains[this.chain]; + } + + async getWeb3(network: string): Promise<{ rpc: CryptoRpc; web3: Web3 }> { + console.log('getWeb3'); + console.log('network ' + network); + try { + if (RSKStateProvider.rpcs[network]) { + console.log('getBlockNumber'); + await RSKStateProvider.rpcs[network].web3.eth.getBlockNumber(); + console.log('getBlockNumber 2'); + } + } catch (e) { + console.log(e); + console.log('catch getWeb3'); + delete RSKStateProvider.rpcs[network]; + } + if (!RSKStateProvider.rpcs[network]) { + console.log('making a new connection'); + const rpcConfig = { ...this.config[network].provider, chain: this.chain, currencyConfig: {} }; + console.log('rpcConfig: '); + logger.info(rpcConfig); + console.log('config: '); + logger.info(this.config[network]); + console.log('provider: '); + logger.info(this.config[network].provider); + const rpc = new CryptoRpc(rpcConfig, {}).get(this.chain); + RSKStateProvider.rpcs[network] = { rpc, web3: rpc.web3 }; + } + return RSKStateProvider.rpcs[network]; + } + + async erc20For(network: string, address: string) { + const { web3 } = await this.getWeb3(network); + const contract = new web3.eth.Contract(ERC20Abi as AbiItem[], address); + return contract; + } + + async getERC20TokenInfo(network: string, tokenAddress: string) { + const token = await RSK.erc20For(network, tokenAddress); + const [name, decimals, symbol] = await Promise.all([ + token.methods.name().call(), + token.methods.decimals().call(), + token.methods.symbol().call() + ]); + + return { + name, + decimals, + symbol + }; + } + + async getFee(params) { + let { network, target = 4 } = params; + const chain = this.chain; + if (network === 'livenet') { + network = 'mainnet'; + } + + const cacheKey = `getFee-${chain}-${network}-${target}`; + return CacheStorage.getGlobalOrRefresh( + cacheKey, + async () => { + const txs = await RskTransactionStorage.collection + .find({ chain, network, blockHeight: { $gt: 0 } }) + .project({ gasPrice: 1, blockHeight: 1 }) + .sort({ blockHeight: -1 }) + .limit(20 * 200) + .toArray(); + + const blockGasPrices = txs + .map(tx => Number(tx.gasPrice)) + .filter(gasPrice => gasPrice) + .sort((a, b) => b - a); + + const whichQuartile = Math.min(target, 4) || 1; + const quartileMedian = StatsUtil.getNthQuartileMedian(blockGasPrices, whichQuartile); + + const roundedGwei = (quartileMedian / 1e9).toFixed(2); + const gwei = Number(roundedGwei) || 0; + const feerate = gwei * 1e9; + return { feerate, blocks: target }; + }, + CacheStorage.Times.Minute + ); + } + + async getBalanceForAddress(params: GetBalanceForAddressParams) { + const { chain, network, address } = params; + const { web3 } = await this.getWeb3(network); + const tokenAddress = params.args && params.args.tokenAddress; + const addressLower = address.toLowerCase(); + const cacheKey = tokenAddress + ? `getBalanceForAddress-${chain}-${network}-${addressLower}-${tokenAddress.toLowerCase()}` + : `getBalanceForAddress-${chain}-${network}-${addressLower}`; + const balances = await CacheStorage.getGlobalOrRefresh( + cacheKey, + async () => { + if (tokenAddress) { + const token = await this.erc20For(network, tokenAddress); + const balance = await token.methods.balanceOf(address).call(); + const numberBalance = Number(balance); + return { confirmed: numberBalance, unconfirmed: 0, balance: numberBalance }; + } else { + const balance = await web3.eth.getBalance(address); + const numberBalance = Number(balance); + return { confirmed: numberBalance, unconfirmed: 0, balance: numberBalance }; + } + }, + CacheStorage.Times.Minute + ); + return balances; + } + + async getLocalTip({ chain, network }) { + return RskBlockStorage.getLocalTip({ chain, network }); + } + + async getReceipt(network: string, txid: string) { + const { web3 } = await this.getWeb3(network); + return web3.eth.getTransactionReceipt(txid); + } + + async populateReceipt(tx: MongoBound) { + if (!tx.receipt) { + const receipt = await this.getReceipt(tx.network, tx.txid); + if (receipt) { + const fee = receipt.gasUsed * tx.gasPrice; + await RskTransactionStorage.collection.updateOne({ _id: tx._id }, { $set: { receipt, fee } }); + tx.receipt = receipt; + tx.fee = fee; + } + } + return tx; + } + + async getTransaction(params: StreamTransactionParams) { + try { + let { chain, network, txId } = params; + if (typeof txId !== 'string' || !chain || !network) { + throw new Error('Missing required param'); + } + network = network.toLowerCase(); + let query = { chain, network, txid: txId }; + const tip = await this.getLocalTip(params); + const tipHeight = tip ? tip.height : 0; + let found = await RskTransactionStorage.collection.findOne(query); + if (found) { + let confirmations = 0; + if (found.blockHeight && found.blockHeight >= 0) { + confirmations = tipHeight - found.blockHeight + 1; + } + found = await this.populateReceipt(found); + const convertedTx = RskTransactionStorage._apiTransform(found, { object: true }) as RskTransactionJSON; + return { ...convertedTx, confirmations }; + } else { + return undefined; + } + } catch (err) { + console.error(err); + } + return undefined; + } + + async broadcastTransaction(params: BroadcastTransactionParams) { + const { network, rawTx } = params; + const { web3 } = await this.getWeb3(network); + const rawTxs = typeof rawTx === 'string' ? [rawTx] : rawTx; + const txids = new Array(); + for (const tx of rawTxs) { + const txid = await new Promise((resolve, reject) => { + web3.eth + .sendSignedTransaction(tx) + .on('transactionHash', resolve) + .on('error', reject) + .catch(e => { + logger.error(e); + reject(e); + }); + }); + txids.push(txid); + } + return txids.length === 1 ? txids[0] : txids; + } + + async streamAddressTransactions(params: StreamAddressUtxosParams) { + const { req, res, args, chain, network, address } = params; + const { limit, /*since,*/ tokenAddress } = args; + if (!args.tokenAddress) { + const query = { + $or: [ + { chain, network, from: address }, + { chain, network, to: address } + ] + }; + + // NOTE: commented out since and paging for now b/c they were causing extra long query times on insight. + // The case where an address has >1000 txns is an edge case ATM and can be addressed later + Storage.apiStreamingFind(RskTransactionStorage, query, { limit /*since, paging: '_id'*/ }, req!, res!); + } else { + try { + const tokenTransfers = await this.getErc20Transfers(network, address, tokenAddress); + res!.json(tokenTransfers); + } catch (e) { + res!.status(500).send(e); + } + } + } + + async streamTransactions(params: StreamTransactionsParams) { + const { chain, network, req, res, args } = params; + let { blockHash, blockHeight } = args; + if (!chain || !network) { + throw new Error('Missing chain or network'); + } + let query: any = { + chain, + network: network.toLowerCase() + }; + if (blockHeight !== undefined) { + query.blockHeight = Number(blockHeight); + } + if (blockHash !== undefined) { + query.blockHash = blockHash; + } + const tip = await this.getLocalTip(params); + const tipHeight = tip ? tip.height : 0; + return Storage.apiStreamingFind(RskTransactionStorage, query, args, req, res, t => { + let confirmations = 0; + if (t.blockHeight !== undefined && t.blockHeight >= 0) { + confirmations = tipHeight - t.blockHeight + 1; + } + const convertedTx = RskTransactionStorage._apiTransform(t, { object: true }) as Partial; + return JSON.stringify({ ...convertedTx, confirmations }); + }); + } + + async getWalletBalance(params: GetWalletBalanceParams) { + const { network } = params; + if (params.wallet._id === undefined) { + throw new Error('Wallet balance can only be retrieved for wallets with the _id property'); + } + let addresses = await this.getWalletAddresses(params.wallet._id); + let addressBalancePromises = addresses.map(({ address }) => + this.getBalanceForAddress({ chain: this.chain, network, address, args: params.args }) + ); + let addressBalances = await Promise.all<{ confirmed: number; unconfirmed: number; balance: number }>( + addressBalancePromises + ); + let balance = addressBalances.reduce( + (prev, cur) => ({ + unconfirmed: prev.unconfirmed + Number(cur.unconfirmed), + confirmed: prev.confirmed + Number(cur.confirmed), + balance: prev.balance + Number(cur.balance) + }), + { unconfirmed: 0, confirmed: 0, balance: 0 } + ); + return balance; + } + + getWalletTransactionQuery(params: StreamWalletTransactionsParams) { + const { chain, network, wallet, args } = params; + let query = { + chain, + network, + wallets: wallet._id, + 'wallets.0': { $exists: true }, + blockHeight: { $gt: -3 } // Exclude invalid transactions + } as any; + if (args) { + if (args.startBlock || args.endBlock) { + query.$or = []; + if (args.includeMempool) { + query.$or.push({ blockHeight: SpentHeightIndicators.pending }); + } + let blockRangeQuery = {} as any; + if (args.startBlock) { + blockRangeQuery.$gte = Number(args.startBlock); + } + if (args.endBlock) { + blockRangeQuery.$lte = Number(args.endBlock); + } + query.$or.push({ blockHeight: blockRangeQuery }); + } else { + if (args.startDate) { + const startDate = new Date(args.startDate); + if (startDate.getTime()) { + query.blockTimeNormalized = { $gte: new Date(args.startDate) }; + } + } + if (args.endDate) { + const endDate = new Date(args.endDate); + if (endDate.getTime()) { + query.blockTimeNormalized = query.blockTimeNormalized || {}; + query.blockTimeNormalized.$lt = new Date(args.endDate); + } + } + } + if (args.includeInvalidTxs) delete query.blockHeight; + } + return query; + } + + async streamWalletTransactions(params: StreamWalletTransactionsParams) { + const { network, wallet, res, args } = params; + const { web3 } = await this.getWeb3(network); + const query = RSK.getWalletTransactionQuery(params); + + let transactionStream = new Readable({ objectMode: true }); + const walletAddresses = (await this.getWalletAddresses(wallet._id!)).map(waddres => waddres.address); + const rskTransactionTransform = new RskListTransactionsStream(walletAddresses); + const populateReceipt = new PopulateReceiptTransform(); + + transactionStream = RskTransactionStorage.collection + .find(query) + .sort({ blockTimeNormalized: 1 }) + .addCursorFlag('noCursorTimeout', true); + + if (!args.tokenAddress && wallet._id) { + const internalTxTransform = new InternalTxRelatedFilterTransform(web3, wallet._id); + transactionStream = transactionStream.pipe(internalTxTransform); + } + + if (args.tokenAddress) { + const erc20Transform = new Erc20RelatedFilterTransform(web3, args.tokenAddress); + transactionStream = transactionStream.pipe(erc20Transform); + } + + transactionStream + .pipe(populateReceipt) + .pipe(rskTransactionTransform) + .pipe(res); + } + + async getErc20Transfers( + network: string, + address: string, + tokenAddress: string, + args: Partial = {} + ): Promise>> { + const token = await this.erc20For(network, tokenAddress); + const [sent, received] = await Promise.all([ + token.getPastEvents('Transfer', { + filter: { _from: address }, + fromBlock: args.startBlock || 0, + toBlock: args.endBlock || 'latest' + }), + token.getPastEvents('Transfer', { + filter: { _to: address }, + fromBlock: args.startBlock || 0, + toBlock: args.endBlock || 'latest' + }) + ]); + return this.convertTokenTransfers([...sent, ...received]); + } + + convertTokenTransfers(tokenTransfers: Array) { + return tokenTransfers.map(this.convertTokenTransfer); + } + + convertTokenTransfer(transfer: ERC20Transfer) { + const { blockHash, blockNumber, transactionHash, returnValues, transactionIndex } = transfer; + return { + blockHash, + blockNumber, + transactionHash, + transactionIndex, + hash: transactionHash, + from: returnValues['_from'], + to: returnValues['_to'], + value: returnValues['_value'] + } as Partial; + } + + async getAccountNonce(network: string, address: string) { + const { web3 } = await this.getWeb3(network); + const count = await web3.eth.getTransactionCount(address); + return count; + /* + *return EthTransactionStorage.collection.countDocuments({ + * chain: 'ETH', + * network, + * from: address, + * blockHeight: { $gt: -1 } + *}); + */ + } + + async getWalletTokenTransactions( + network: string, + walletId: ObjectID, + tokenAddress: string, + args: StreamWalletTransactionsArgs + ) { + const addresses = await this.getWalletAddresses(walletId); + const allTokenQueries = Array>>>(); + for (const walletAddress of addresses) { + const transfers = this.getErc20Transfers(network, walletAddress.address, tokenAddress, args); + allTokenQueries.push(transfers); + } + let batches = await Promise.all(allTokenQueries); + let txs = batches.reduce((agg, batch) => agg.concat(batch)); + return txs.sort((tx1, tx2) => tx1.blockNumber! - tx2.blockNumber!); + } + + async estimateGas(params): Promise { + return new Promise(async (resolve, reject) => { + try { + let { network, value, from, data, gasPrice, to } = params; + const { web3 } = await this.getWeb3(network); + const dataDecoded = RskTransactionStorage.abiDecode(data); + + if (dataDecoded && dataDecoded.type === 'INVOICE' && dataDecoded.name === 'pay') { + value = dataDecoded.params[0].value; + gasPrice = dataDecoded.params[1].value; + } + + const opts = { + method: 'eth_estimateGas', + params: [ + { + data, + to: to && to.toLowerCase(), + from: from && from.toLowerCase(), + gasPrice: web3.utils.toHex(gasPrice), + value: web3.utils.toHex(value) + } + ], + jsonrpc: '2.0', + id: 1 + }; + + let provider = web3.currentProvider as any; + provider.send(opts, (err, data) => { + if (err) return reject(err); + if (!data.result) return reject(data.message); + return resolve(Number(data.result)); + }); + } catch (err) { + return reject(err); + } + }); + } + + async getBlocks(params: GetBlockParams) { + const { query, options } = this.getBlocksQuery(params); + let cursor = RskBlockStorage.collection.find(query, options).addCursorFlag('noCursorTimeout', true); + if (options.sort) { + cursor = cursor.sort(options.sort); + } + let blocks = await cursor.toArray(); + const tip = await this.getLocalTip(params); + const tipHeight = tip ? tip.height : 0; + const blockTransform = (b: IRskBlock) => { + let confirmations = 0; + if (b.height && b.height >= 0) { + confirmations = tipHeight - b.height + 1; + } + const convertedBlock = RskBlockStorage._apiTransform(b, { object: true }) as IRskBlock; + return { ...convertedBlock, confirmations }; + }; + return blocks.map(blockTransform); + } + + async updateWallet(params: UpdateWalletParams) { + const { chain, network } = params; + const addressBatches = partition(params.addresses, 500); + for (let addressBatch of addressBatches) { + const walletAddressInserts = addressBatch.map(address => { + return { + insertOne: { + document: { chain, network, wallet: params.wallet._id, address, processed: false } + } + }; + }); + + try { + await WalletAddressStorage.collection.bulkWrite(walletAddressInserts); + } catch (err) { + if (err.code !== 11000) { + throw err; + } + } + + await RskTransactionStorage.collection.updateMany( + { + $or: [ + { chain, network, from: { $in: addressBatch } }, + { chain, network, to: { $in: addressBatch } }, + { chain, network, 'internal.action.to': { $in: addressBatch } }, + { + chain, + network, + 'abiType.params.0.value': { $in: addressBatch.map(address => address.toLowerCase()) }, + 'abiType.type': 'ERC20', + 'abiType.name': 'transfer' + } + ] + }, + { $addToSet: { wallets: params.wallet._id } } + ); + + await WalletAddressStorage.collection.updateMany( + { chain, network, address: { $in: addressBatch }, wallet: params.wallet._id }, + { $set: { processed: true } } + ); + } + } + + async getCoinsForTx() { + return { + inputs: [], + outputs: [] + }; + } +} + +export const RSK = new RSKStateProvider(); diff --git a/packages/bitcore-node/src/modules/rsk/api/erc20Transform.ts b/packages/bitcore-node/src/modules/rsk/api/erc20Transform.ts new file mode 100644 index 00000000000..48faa0b0243 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/api/erc20Transform.ts @@ -0,0 +1,61 @@ +import { Transform } from 'stream'; +import Web3 from 'web3'; +import { MongoBound } from '../../../models/base'; +import { IRskTransaction } from '../types'; + +export class Erc20RelatedFilterTransform extends Transform { + constructor(private web3: Web3, private tokenAddress: string) { + super({ objectMode: true }); + } + + async _transform(tx: MongoBound, _, done) { + if ( + tx.abiType && + tx.abiType.type === 'ERC20' && + tx.abiType.name === 'transfer' && + tx.to.toLowerCase() === this.tokenAddress.toLowerCase() + ) { + tx.value = tx.abiType!.params[1].value as any; + tx.to = this.web3.utils.toChecksumAddress(tx.abiType!.params[0].value); + } else if ( + tx.abiType && + tx.abiType.type === 'INVOICE' && + tx.abiType.name === 'pay' && + tx.abiType.params[8].value.toLowerCase() === this.tokenAddress.toLowerCase() + ) { + tx.value = tx.abiType!.params[0].value as any; + } else if (tx.internal && tx.internal.length > 0) { + try { + const tokenRelatedIncomingInternalTxs = tx.internal.filter( + (internalTx: any) => + internalTx.action.to && this.tokenAddress.toLowerCase() === internalTx.action.to.toLowerCase() + ); + for (const internalTx of tokenRelatedIncomingInternalTxs) { + if ( + internalTx.abiType && + (internalTx.abiType.name === 'transfer' || internalTx.abiType.name === 'transferFrom') + ) { + const _tx = Object.assign({}, tx); + for (const element of internalTx.abiType.params) { + if (element.name === '_value') _tx.value = element.value as any; + if (element.name === '_to') _tx.to = this.web3.utils.toChecksumAddress(element.value); + if (element.name === '_from') _tx.from = this.web3.utils.toChecksumAddress(element.value); + else if (internalTx.action.from && internalTx.abiType && internalTx.abiType.name == 'transfer') { + _tx.from = this.web3.utils.toChecksumAddress(internalTx.action.from); + } + } + this.push(_tx); + } + } + return done(); + } catch (err) { + console.error(err); + return done(); + } + } else { + return done(); + } + this.push(tx); + return done(); + } +} diff --git a/packages/bitcore-node/src/modules/rsk/api/ethMultisigTransform.ts b/packages/bitcore-node/src/modules/rsk/api/ethMultisigTransform.ts new file mode 100644 index 00000000000..95f51a748b0 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/api/ethMultisigTransform.ts @@ -0,0 +1,62 @@ +import { Transform } from 'stream'; +import Web3 from 'web3'; +import { MongoBound } from '../../../models/base'; +import { IRskTransaction } from '../types'; + +export class EthMultisigRelatedFilterTransform extends Transform { + constructor(private web3: Web3, private multisigContractAddress: string, private tokenAddress: string) { + super({ objectMode: true }); + } + + async _transform(tx: MongoBound, _, done) { + if (tx.internal && tx.internal.length > 0 && !this.tokenAddress) { + const walletRelatedIncomingInternalTxs = tx.internal.filter( + (internalTx: any) => this.multisigContractAddress === this.web3.utils.toChecksumAddress(internalTx.action.to) + ); + const walletRelatedOutgoingInternalTxs = tx.internal.filter( + (internalTx: any) => this.multisigContractAddress === this.web3.utils.toChecksumAddress(internalTx.action.from) + ); + walletRelatedIncomingInternalTxs.forEach(internalTx => { + const _tx = Object.assign({}, tx); + _tx.value = Number(internalTx.action.value); + _tx.to = this.web3.utils.toChecksumAddress(internalTx.action.to); + if (internalTx.action.from) _tx.from = this.web3.utils.toChecksumAddress(internalTx.action.from); + this.push(_tx); + }); + walletRelatedOutgoingInternalTxs.forEach(internalTx => { + const _tx = Object.assign({}, tx); + _tx.value = Number(internalTx.action.value); + _tx.to = this.web3.utils.toChecksumAddress(internalTx.action.to); + if (internalTx.action.from) _tx.from = this.web3.utils.toChecksumAddress(internalTx.action.from); + this.push(_tx); + }); + if (walletRelatedIncomingInternalTxs.length || walletRelatedOutgoingInternalTxs.length) return done(); + } else if ( + tx.abiType && + tx.abiType.type === 'ERC20' && + tx.abiType.name === 'transfer' && + this.tokenAddress && + tx.to.toLowerCase() === this.tokenAddress.toLowerCase() + ) { + tx.value = tx.abiType!.params[1].value as any; + tx.to = this.web3.utils.toChecksumAddress(tx.abiType!.params[0].value); + } else if ( + tx.internal && + tx.internal.length > 0 && + tx.internal[0].abiType && + tx.internal[0].abiType.type === 'ERC20' && + tx.internal[0].abiType.name === 'transfer' && + tx.internal[0].action.to && + tx.internal[0].action.from && + tx.internal[0].action.to.toLowerCase() === this.tokenAddress.toLowerCase() + ) { + tx.value = tx.internal[0].abiType!.params[1].value as any; + tx.to = this.web3.utils.toChecksumAddress(tx.internal[0].abiType!.params[0].value); + tx.from = this.web3.utils.toChecksumAddress(tx.internal[0].action.from); + } else if (tx.to !== this.multisigContractAddress || (tx.to === this.multisigContractAddress && tx.abiType)) { + return done(); + } + this.push(tx); + return done(); + } +} diff --git a/packages/bitcore-node/src/modules/rsk/api/gnosis.ts b/packages/bitcore-node/src/modules/rsk/api/gnosis.ts new file mode 100644 index 00000000000..7bfa9ef35ec --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/api/gnosis.ts @@ -0,0 +1,202 @@ +import { Readable } from 'stream'; +import { Transaction } from 'web3-eth'; +import { AbiItem } from 'web3-utils'; +import { Config } from '../../../services/config'; +import { StreamWalletTransactionsParams } from '../../../types/namespaces/ChainStateProvider'; +import { MultisigAbi } from '../abi/multisig'; +import { RskBlockStorage } from '../models/block'; +import { RskTransactionStorage } from '../models/transaction'; +import { EventLog, RSK } from './csp'; +import { EthMultisigRelatedFilterTransform } from './ethMultisigTransform'; +import { PopulateReceiptTransform } from './populateReceiptTransform'; +import { RskListTransactionsStream } from './transform'; + +interface MULTISIGInstantiation + extends EventLog<{ + [key: string]: string; + }> {} + +interface MULTISIGTxInfo + extends EventLog<{ + [key: string]: string; + }> {} + +export class GnosisApi { + public gnosisFactories = { + // TODO: add the ones from RSK + testnet: '0x2C992817e0152A65937527B774c7A99a84603045', + mainnet: '0x6e95C8E8557AbC08b46F3c347bA06F8dC012763f' + }; + + private ETH_MULTISIG_TX_PROPOSAL_EXPIRE_TIME = 48 * 3600 * 1000; + + async multisigFor(network: string, address: string) { + const { web3 } = await RSK.getWeb3(network); + const contract = new web3.eth.Contract(MultisigAbi as AbiItem[], address); + return contract; + } + + async getMultisigContractInstantiationInfo( + network: string, + sender: string, + txId: string + ): Promise[]> { + const { web3 } = await RSK.getWeb3(network); + const networkConfig = Config.chainConfig({ chain: 'RSK', network }); + const { gnosisFactory = this.gnosisFactories[network] } = networkConfig; + let query = { chain: 'RSK', network, txid: txId }; + const found = await RskTransactionStorage.collection.findOne(query); + const blockHeight = found && found.blockHeight ? found.blockHeight : null; + if (!blockHeight || blockHeight < 0) return Promise.resolve([]); + const contract = await this.multisigFor(network, gnosisFactory); + const contractInfo = await contract.getPastEvents('ContractInstantiation', { + fromBlock: web3.utils.toHex(blockHeight), + toBlock: web3.utils.toHex(blockHeight) + }); + return this.convertMultisigContractInstantiationInfo( + contractInfo.filter(info => info.returnValues.sender.toLowerCase() === sender.toLowerCase()) + ); + } + + convertMultisigContractInstantiationInfo(contractInstantiationInfo: Array) { + return contractInstantiationInfo.map(this.convertContractInstantiationInfo); + } + + convertContractInstantiationInfo(transfer: MULTISIGInstantiation) { + const { blockHash, blockNumber, transactionHash, returnValues, transactionIndex } = transfer; + return { + blockHash, + blockNumber, + transactionHash, + transactionIndex, + hash: transactionHash, + sender: returnValues['sender'], + instantiation: returnValues['instantiation'] + } as Partial; + } + + async getMultisigTxpsInfo(network: string, multisigContractAddress: string): Promise[]> { + const contract = await this.multisigFor(network, multisigContractAddress); + const time = Math.floor(Date.now()) - this.ETH_MULTISIG_TX_PROPOSAL_EXPIRE_TIME; + const [block] = await RskBlockStorage.collection + .find({ + chain: 'RSK', + network, + timeNormalized: { $gte: new Date(time) } + }) + .limit(1) + .toArray(); + + const blockHeight = block!.height; + const [confirmationInfo, revocationInfo, executionInfo, executionFailure] = await Promise.all([ + contract.getPastEvents('Confirmation', { + fromBlock: blockHeight, + toBlock: 'latest' + }), + contract.getPastEvents('Revocation', { + fromBlock: blockHeight, + toBlock: 'latest' + }), + contract.getPastEvents('Execution', { + fromBlock: blockHeight, + toBlock: 'latest' + }), + contract.getPastEvents('ExecutionFailure', { + fromBlock: blockHeight, + toBlock: 'latest' + }) + ]); + + const executionTransactionIdArray = executionInfo.map(i => i.returnValues.transactionId); + const contractTransactionsInfo = [...confirmationInfo, ...revocationInfo, ...executionFailure]; + const multisigTxpsInfo = contractTransactionsInfo.filter( + i => !executionTransactionIdArray.includes(i.returnValues.transactionId) + ); + return this.convertMultisigTxpsInfo(multisigTxpsInfo); + } + + convertMultisigTxpsInfo(multisigTxpsInfo: Array) { + return multisigTxpsInfo.map(this.convertTxpsInfo); + } + + convertTxpsInfo(transfer: MULTISIGTxInfo) { + const { blockHash, blockNumber, transactionHash, returnValues, transactionIndex, event } = transfer; + return { + blockHash, + blockNumber, + transactionHash, + transactionIndex, + hash: transactionHash, + sender: returnValues['sender'], + transactionId: returnValues['transactionId'], + event + } as Partial; + } + + async getMultisigEthInfo(network: string, multisigContractAddress: string) { + const contract: any = await this.multisigFor(network, multisigContractAddress); + const owners = await contract.methods.getOwners().call(); + const required = await contract.methods.required().call(); + return { + owners, + required + }; + } + + async streamGnosisWalletTransactions(params: { multisigContractAddress: string } & StreamWalletTransactionsParams) { + const { multisigContractAddress, network, res, args } = params; + const { web3 } = await RSK.getWeb3(network); + const transactionQuery = RSK.getWalletTransactionQuery(params); + delete transactionQuery.wallets; + delete transactionQuery['wallets.0']; + let query; + if (args.tokenAddress) { + query = { + $or: [ + { + ...transactionQuery, + to: args.tokenAddress, + 'abiType.params.0.value': multisigContractAddress.toLowerCase() + }, + { + ...transactionQuery, + 'internal.action.to': args.tokenAddress.toLowerCase(), + 'internal.action.from': multisigContractAddress.toLowerCase() + } + ] + }; + } else { + query = { + $or: [ + { ...transactionQuery, to: multisigContractAddress }, + { ...transactionQuery, 'internal.action.to': multisigContractAddress.toLowerCase() } + ] + }; + } + + let transactionStream = new Readable({ objectMode: true }); + const rskTransactionTransform = new RskListTransactionsStream([multisigContractAddress, args.tokenAddress]); + const populateReceipt = new PopulateReceiptTransform(); + + transactionStream = RskTransactionStorage.collection + .find(query) + .sort({ blockTimeNormalized: 1 }) + .addCursorFlag('noCursorTimeout', true); + + if (multisigContractAddress) { + const rskMultisigTransform = new EthMultisigRelatedFilterTransform( + web3, + multisigContractAddress, + args.tokenAddress + ); + transactionStream = transactionStream.pipe(rskMultisigTransform); + } + + transactionStream + .pipe(populateReceipt) + .pipe(rskTransactionTransform) + .pipe(res); + } +} + +export const Gnosis = new GnosisApi(); diff --git a/packages/bitcore-node/src/modules/rsk/api/internalTxTransform.ts b/packages/bitcore-node/src/modules/rsk/api/internalTxTransform.ts new file mode 100644 index 00000000000..e8d33b0ce4c --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/api/internalTxTransform.ts @@ -0,0 +1,41 @@ +import { Transform } from 'stream'; +import Web3 from 'web3'; +import { MongoBound } from '../../../models/base'; +import { IWalletAddress, WalletAddressStorage } from '../../../models/walletAddress'; +import { IRskTransaction } from '../types'; + +export class InternalTxRelatedFilterTransform extends Transform { + private walletAddresses: IWalletAddress[] = []; + constructor(private web3: Web3, private walletId) { + super({ objectMode: true }); + } + + async _transform(tx: MongoBound, _, done) { + if (tx.internal && tx.internal.length > 0) { + const walletAddresses = await this.getWalletAddresses(tx); + const walletAddressesArray = walletAddresses.map(walletAddress => walletAddress.address); + const walletRelatedInternalTxs = tx.internal.filter((internalTx: any) => + walletAddressesArray.includes(internalTx.action.to) + ); + walletRelatedInternalTxs.forEach(internalTx => { + const _tx = Object.assign({}, tx); + _tx.value = Number(internalTx.action.value); + _tx.to = this.web3.utils.toChecksumAddress(internalTx.action.to); + if (internalTx.action.from) _tx.from = this.web3.utils.toChecksumAddress(internalTx.action.from); + this.push(_tx); + }); + if (walletRelatedInternalTxs.length) return done(); + } + this.push(tx); + return done(); + } + + async getWalletAddresses(tx) { + if (!this.walletAddresses.length) { + this.walletAddresses = await WalletAddressStorage.collection + .find({ chain: tx.chain, network: tx.network, wallet: this.walletId }) + .toArray(); + } + return this.walletAddresses; + } +} diff --git a/packages/bitcore-node/src/modules/rsk/api/populateReceiptTransform.ts b/packages/bitcore-node/src/modules/rsk/api/populateReceiptTransform.ts new file mode 100644 index 00000000000..8caed09c838 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/api/populateReceiptTransform.ts @@ -0,0 +1,18 @@ +import { Transform } from 'stream'; +import { MongoBound } from '../../../models/base'; +import { IRskTransaction } from '../types'; +import { RSK } from './csp'; + +export class PopulateReceiptTransform extends Transform { + constructor() { + super({ objectMode: true }); + } + + async _transform(tx: MongoBound, _, done) { + try { + tx = await RSK.populateReceipt(tx); + } catch (e) {} + this.push(tx); + return done(); + } +} diff --git a/packages/bitcore-node/src/modules/rsk/api/rsk-routes.ts b/packages/bitcore-node/src/modules/rsk/api/rsk-routes.ts new file mode 100644 index 00000000000..77036c184c6 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/api/rsk-routes.ts @@ -0,0 +1,85 @@ +import { Router } from 'express'; +import logger from '../../../logger'; +import { RSK } from './csp'; +import { Gnosis } from './gnosis'; +export const RskRoutes = Router(); + +RskRoutes.get('/api/RSK/:network/address/:address/txs/count', async (req, res) => { + let { address, network } = req.params; + try { + const nonce = await RSK.getAccountNonce(network, address); + res.json({ nonce }); + } catch (err) { + logger.error('Nonce Error::' + err); + res.status(500).send(err); + } +}); + +RskRoutes.post('/api/RSK/:network/gas', async (req, res) => { + const { from, to, value, data, gasPrice } = req.body; + const { network } = req.params; + try { + const gasLimit = await RSK.estimateGas({ network, from, to, value, data, gasPrice }); + res.json(gasLimit); + } catch (err) { + res.status(500).send(err); + } +}); + +RskRoutes.get('/api/RSK/:network/token/:tokenAddress', async (req, res) => { + const { network, tokenAddress } = req.params; + try { + const tokenInfo = await RSK.getERC20TokenInfo(network, tokenAddress); + res.json(tokenInfo); + } catch (err) { + res.status(500).send(err); + } +}); + +RskRoutes.get('/api/RSK/:network/ethmultisig/info/:multisigContractAddress', async (req, res) => { + const { network, multisigContractAddress } = req.params; + try { + const multisigInfo = await Gnosis.getMultisigEthInfo(network, multisigContractAddress); + res.json(multisigInfo); + } catch (err) { + res.status(500).send(err); + } +}); + +RskRoutes.get('/api/RSK/:network/ethmultisig/:sender/instantiation/:txId', async (req, res) => { + const { network, sender, txId } = req.params; + try { + const multisigInstantiationInfo = await Gnosis.getMultisigContractInstantiationInfo(network, sender, txId); + res.json(multisigInstantiationInfo); + } catch (err) { + res.status(500).send(err); + } +}); + +RskRoutes.get('/api/RSK/:network/ethmultisig/txps/:multisigContractAddress', async (req, res) => { + const { network, multisigContractAddress } = req.params; + try { + const multisigTxpsInfo = await Gnosis.getMultisigTxpsInfo(network, multisigContractAddress); + res.json(multisigTxpsInfo); + } catch (err) { + res.status(500).send(err); + } +}); + +RskRoutes.get('/api/RSK/:network/ethmultisig/transactions/:multisigContractAddress', async (req, res) => { + let { network, multisigContractAddress } = req.params; + const chain = 'RSK'; + try { + return await Gnosis.streamGnosisWalletTransactions({ + chain, + network, + multisigContractAddress, + wallet: {} as any, + req, + res, + args: req.query + }); + } catch (err) { + return res.status(500).send(err); + } +}); diff --git a/packages/bitcore-node/src/modules/rsk/api/transform.ts b/packages/bitcore-node/src/modules/rsk/api/transform.ts new file mode 100644 index 00000000000..2f0b533701e --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/api/transform.ts @@ -0,0 +1,81 @@ +import { Transform } from 'stream'; +import { MongoBound } from '../../../models/base'; +import { IRskTransaction } from '../types'; + +export class RskListTransactionsStream extends Transform { + constructor(private walletAddresses: Array) { + super({ objectMode: true }); + } + + async _transform(transaction: MongoBound, _, done) { + let sending = this.walletAddresses.includes(transaction.from); + if (sending) { + let sendingToOurself = this.walletAddresses.includes(transaction.to); + if (!sendingToOurself) { + this.push( + JSON.stringify({ + id: transaction._id, + txid: transaction.txid, + fee: transaction.fee, + category: 'send', + satoshis: -transaction.value, + height: transaction.blockHeight, + from: transaction.from, + gasPrice: transaction.gasPrice, + gasLimit: transaction.gasLimit, + receipt: transaction.receipt, + address: transaction.to, + blockTime: transaction.blockTimeNormalized, + internal: transaction.internal, + abiType: transaction.abiType, + error: transaction.error + }) + '\n' + ); + } else { + this.push( + JSON.stringify({ + id: transaction._id, + txid: transaction.txid, + fee: transaction.fee, + category: 'move', + satoshis: transaction.value, + height: transaction.blockHeight, + from: transaction.from, + gasPrice: transaction.gasPrice, + gasLimit: transaction.gasLimit, + receipt: transaction.receipt, + address: transaction.to, + blockTime: transaction.blockTimeNormalized, + internal: transaction.internal, + abiType: transaction.abiType, + error: transaction.error + }) + '\n' + ); + } + } else { + const weReceived = this.walletAddresses.includes(transaction.to); + if (weReceived) { + this.push( + JSON.stringify({ + id: transaction._id, + txid: transaction.txid, + fee: transaction.fee, + category: 'receive', + satoshis: transaction.value, + height: transaction.blockHeight, + from: transaction.from, + gasPrice: transaction.gasPrice, + gasLimit: transaction.gasLimit, + receipt: transaction.receipt, + address: transaction.to, + blockTime: transaction.blockTimeNormalized, + internal: transaction.internal, + abiType: transaction.abiType, + error: transaction.error + }) + '\n' + ); + } + } + return done(); + } +} diff --git a/packages/bitcore-node/src/modules/rsk/index.ts b/packages/bitcore-node/src/modules/rsk/index.ts new file mode 100644 index 00000000000..5a9862b0e97 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/index.ts @@ -0,0 +1,15 @@ +import { BaseModule } from '..'; +import { RSKStateProvider } from './api/csp'; +import { RskRoutes } from './api/rsk-routes'; +import { RskP2pWorker } from './p2p/p2p'; +import { RskVerificationPeer } from './p2p/RskVerificationPeer'; + +export default class RSKModule extends BaseModule { + constructor(services: BaseModule['bitcoreServices']) { + super(services); + services.P2P.register('RSK', RskP2pWorker); + services.CSP.registerService('RSK', new RSKStateProvider()); + services.Api.app.use(RskRoutes); + services.Verification.register('RSK', RskVerificationPeer); + } +} diff --git a/packages/bitcore-node/src/modules/rsk/models/block.ts b/packages/bitcore-node/src/modules/rsk/models/block.ts new file mode 100644 index 00000000000..2298aec7ba1 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/models/block.ts @@ -0,0 +1,183 @@ +import { LoggifyClass } from '../../../decorators/Loggify'; +import logger from '../../../logger'; +import { MongoBound } from '../../../models/base'; +import { BaseBlock } from '../../../models/baseBlock'; +import { EventStorage } from '../../../models/events'; +import { StorageService } from '../../../services/storage'; +import { IBlock } from '../../../types/Block'; +import { TransformOptions } from '../../../types/TransformOptions'; +import { IRskBlock, IRskTransaction } from '../types'; +import { RskTransactionStorage } from './transaction'; + +@LoggifyClass +export class RskBlockModel extends BaseBlock { + constructor(storage?: StorageService) { + super(storage); + } + + async onConnect() { + super.onConnect(); + } + + async addBlock(params: { + block: IRskBlock; + transactions: IRskTransaction[]; + parentChain?: string; + forkHeight?: number; + initialSyncComplete: boolean; + chain: string; + network: string; + }) { + const { block, chain, network } = params; + + let reorg = false; + const headers = await this.validateLocatorHashes({ chain, network }); + if (headers.length) { + const last = headers[headers.length - 1]; + reorg = await this.handleReorg({ block: last, chain, network }); + } + + reorg = reorg || (await this.handleReorg({ block, chain, network })); + + if (reorg) { + return Promise.reject('reorg'); + } + return this.processBlock(params); + } + + async processBlock(params: { + block: IRskBlock; + transactions: IRskTransaction[]; + parentChain?: string; + forkHeight?: number; + initialSyncComplete: boolean; + chain: string; + network: string; + }) { + const { chain, network, transactions, parentChain, forkHeight, initialSyncComplete } = params; + const blockOp = await this.getBlockOp(params); + const convertedBlock = blockOp.updateOne.update.$set; + const { height, timeNormalized, time } = convertedBlock; + + const previousBlock = await this.collection.findOne({ hash: convertedBlock.previousBlockHash, chain, network }); + + await this.collection.bulkWrite([blockOp]); + if (previousBlock) { + await this.collection.updateOne( + { chain, network, hash: previousBlock.hash }, + { $set: { nextBlockHash: convertedBlock.hash } } + ); + logger.debug('Updating previous block.nextBlockHash ', convertedBlock.hash); + } + + await RskTransactionStorage.batchImport({ + txs: transactions, + blockHash: convertedBlock.hash, + blockTime: new Date(time), + blockTimeNormalized: new Date(timeNormalized), + height, + chain, + network, + parentChain, + forkHeight, + initialSyncComplete + }); + + if (initialSyncComplete) { + EventStorage.signalBlock(convertedBlock); + } + + await this.collection.updateOne({ hash: convertedBlock.hash, chain, network }, { $set: { processed: true } }); + } + + async getBlockOp(params: { block: IRskBlock; chain: string; network: string }) { + const { block, chain, network } = params; + const blockTime = block.time; + const prevHash = block.previousBlockHash; + + const previousBlock = await this.collection.findOne({ hash: prevHash, chain, network }); + + const timeNormalized = (() => { + const prevTime = previousBlock ? new Date(previousBlock.timeNormalized) : null; + if (prevTime && blockTime.getTime() <= prevTime.getTime()) { + return new Date(prevTime.getTime() + 1); + } else { + return blockTime; + } + })(); + + const height = block.height; + logger.debug('Setting blockheight', height); + return { + updateOne: { + filter: { + hash: block.hash, + chain, + network + }, + update: { + $set: { ...block, timeNormalized } + }, + upsert: true + } + }; + } + + async handleReorg(params: { block: IBlock; chain: string; network: string }): Promise { + const { block, chain, network } = params; + const prevHash = block.previousBlockHash; + let localTip = await this.getLocalTip(params); + if (block != null && localTip != null && (localTip.hash === prevHash || localTip.hash === block.hash)) { + return false; + } + if (!localTip || localTip.height === 0) { + return false; + } + if (block) { + const prevBlock = await this.collection.findOne({ chain, network, hash: prevHash }); + if (prevBlock) { + localTip = prevBlock; + } else { + logger.error("Previous block isn't in the DB need to roll back until we have a block in common"); + } + logger.info(`Resetting tip to ${localTip.height - 1}`, { chain, network }); + } + const reorgOps = [ + this.collection.deleteMany({ chain, network, height: { $gte: localTip.height } }), + RskTransactionStorage.collection.deleteMany({ chain, network, blockHeight: { $gte: localTip.height } }) + ]; + await Promise.all(reorgOps); + + logger.debug('Removed data from above blockHeight: ', localTip.height); + return localTip.hash !== prevHash; + } + + _apiTransform(block: Partial>, options?: TransformOptions): any { + const transform = { + _id: block._id, + chain: block.chain, + network: block.network, + hash: block.hash, + height: block.height, + size: block.size, + gasLimit: block.gasLimit, + gasUsed: block.gasUsed, + merkleRoot: block.merkleRoot, + time: block.time, + timeNormalized: block.timeNormalized, + nonce: block.nonce, + previousBlockHash: block.previousBlockHash, + nextBlockHash: block.nextBlockHash, + reward: block.reward, + transactionCount: block.transactionCount, + difficulty: block.difficulty, + totalDifficulty: block.totalDifficulty + }; + if (options && options.object) { + return transform; + } + return JSON.stringify(transform); + } +} + +export let RskBlockStorage = new RskBlockModel(); diff --git a/packages/bitcore-node/src/modules/rsk/models/transaction.ts b/packages/bitcore-node/src/modules/rsk/models/transaction.ts new file mode 100644 index 00000000000..c6fc411fcb6 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/models/transaction.ts @@ -0,0 +1,330 @@ +import { ObjectID } from 'bson'; +import * as _ from 'lodash'; +import { LoggifyClass } from '../../../decorators/Loggify'; +import logger from '../../../logger'; +import { MongoBound } from '../../../models/base'; +import { BaseTransaction } from '../../../models/baseTransaction'; +import { CacheStorage } from '../../../models/cache'; +import { EventStorage } from '../../../models/events'; +import { WalletAddressStorage } from '../../../models/walletAddress'; +import { Config } from '../../../services/config'; +import { Storage, StorageService } from '../../../services/storage'; +import { SpentHeightIndicators } from '../../../types/Coin'; +import { StreamingFindOptions } from '../../../types/Query'; +import { TransformOptions } from '../../../types/TransformOptions'; +import { valueOrDefault } from '../../../utils/check'; +import { partition } from '../../../utils/partition'; +import { ERC20Abi } from '../abi/erc20'; +import { ERC721Abi } from '../abi/erc721'; +import { InvoiceAbi } from '../abi/invoice'; +import { MultisigAbi } from '../abi/multisig'; + +import { IRskTransaction, RskTransactionJSON } from '../types'; + +function requireUncached(module) { + delete require.cache[require.resolve(module)]; + return require(module); +} + +const Erc20Decoder = requireUncached('abi-decoder'); +Erc20Decoder.addABI(ERC20Abi); +function getErc20Decoder() { + return Erc20Decoder; +} + +const Erc721Decoder = requireUncached('abi-decoder'); +Erc721Decoder.addABI(ERC721Abi); +function getErc721Decoder() { + return Erc721Decoder; +} + +const InvoiceDecoder = requireUncached('abi-decoder'); +InvoiceDecoder.addABI(InvoiceAbi); +function getInvoiceDecoder() { + return InvoiceDecoder; +} + +const MultisigDecoder = requireUncached('abi-decoder'); +MultisigDecoder.addABI(MultisigAbi); +function getMultisigDecoder() { + return MultisigDecoder; +} + +@LoggifyClass +export class RskTransactionModel extends BaseTransaction { + constructor(storage: StorageService = Storage) { + super(storage); + } + + async onConnect() { + super.onConnect(); + this.collection.createIndex({ chain: 1, network: 1, to: 1 }, { background: true, sparse: true }); + this.collection.createIndex({ chain: 1, network: 1, from: 1 }, { background: true, sparse: true }); + this.collection.createIndex({ chain: 1, network: 1, from: 1, nonce: 1 }, { background: true, sparse: true }); + // Commented out because it conflicts with existing ETH index + // this.collection.createIndex( + // { chain: 1, network: 1, 'abiType.params.0.value': 1, blockTimeNormalized: 1 }, + // { + // background: true, + // partialFilterExpression: { chain: 'RSK', 'abiType.type': 'ERC20', 'abiType.name': 'transfer' } + // } + // ); + this.collection.createIndex( + { chain: 1, network: 1, 'internal.action.to': 1 }, + { + background: true, + sparse: true + } + ); + } + + async batchImport(params: { + txs: Array; + height: number; + mempoolTime?: Date; + blockTime?: Date; + blockHash?: string; + blockTimeNormalized?: Date; + parentChain?: string; + forkHeight?: number; + chain: string; + network: string; + initialSyncComplete: boolean; + }) { + const operations = [] as Array>; + operations.push(this.pruneMempool({ ...params })); + const txOps = await this.addTransactions({ ...params }); + logger.debug('Writing Transactions', txOps.length); + operations.push( + ...partition(txOps, txOps.length / Config.get().maxPoolSize).map(txBatch => + this.collection.bulkWrite( + txBatch.map(op => this.toMempoolSafeUpsert(op, params.height)), + { ordered: false } + ) + ) + ); + await Promise.all(operations); + + if (params.initialSyncComplete) { + await this.expireBalanceCache(txOps); + } + + // Create events for mempool txs + if (params.height < SpentHeightIndicators.minimum) { + for (let op of txOps) { + const filter = op.updateOne.filter; + const tx = { ...op.updateOne.update.$set, ...filter } as IRskTransaction; + await EventStorage.signalTx(tx); + await EventStorage.signalAddressCoin({ + address: tx.to, + coin: { value: tx.value, address: tx.to, chain: params.chain, network: params.network, mintTxid: tx.txid } + }); + } + } + } + + async expireBalanceCache(txOps: Array) { + for (const op of txOps) { + let batch = new Array<{ multisigContractAdress?: string; tokenAddress?: string; address: string }>(); + const { chain, network } = op.updateOne.filter; + const { from, to, abiType, internal } = op.updateOne.update.$set; + batch = batch.concat([{ address: from }, { address: to }]); + if (abiType && abiType.type === 'ERC20' && abiType.params.length) { + batch.push({ address: from, tokenAddress: to }); + batch.push({ address: abiType.params[0].value, tokenAddress: to }); + } + + if (internal && internal.length > 0) { + internal.forEach(i => { + if (i.action.to) batch.push({ address: i.action.to }); + if (i.action.from) batch.push({ address: i.action.from }); + }); + } + + for (const payload of batch) { + const lowerAddress = payload.address.toLowerCase(); + const cacheKey = payload.tokenAddress + ? `getBalanceForAddress-${chain}-${network}-${lowerAddress}-${to.toLowerCase()}` + : `getBalanceForAddress-${chain}-${network}-${lowerAddress}`; + await CacheStorage.expire(cacheKey); + } + } + } + + async addTransactions(params: { + txs: Array; + height: number; + blockTime?: Date; + blockHash?: string; + blockTimeNormalized?: Date; + parentChain?: string; + forkHeight?: number; + initialSyncComplete: boolean; + chain: string; + network: string; + mempoolTime?: Date; + }) { + let { blockTimeNormalized, chain, height, network, parentChain, forkHeight } = params; + if (parentChain && forkHeight && height < forkHeight) { + const parentTxs = await RskTransactionStorage.collection + .find({ blockHeight: height, chain: parentChain, network }) + .toArray(); + return parentTxs.map(parentTx => { + return { + updateOne: { + filter: { txid: parentTx.txid, chain, network }, + update: { + $set: { + ...parentTx, + wallets: new Array() + } + }, + upsert: true, + forceServerObjectId: true + } + }; + }); + } else { + return Promise.all( + params.txs.map(async (tx: IRskTransaction) => { + const { to, txid, from } = tx; + const sentWallets = await WalletAddressStorage.collection.find({ chain, network, address: from }).toArray(); + const receivedWallets = await WalletAddressStorage.collection.find({ chain, network, address: to }).toArray(); + const wallets = _.uniqBy( + sentWallets.concat(receivedWallets).map(w => w.wallet), + w => w.toHexString() + ); + + return { + updateOne: { + filter: { txid, chain, network }, + update: { + $set: { + ...tx, + blockTimeNormalized, + wallets + } + }, + upsert: true, + forceServerObjectId: true + } + }; + }) + ); + } + } + + async pruneMempool(params: { + txs: Array; + height: number; + parentChain?: string; + forkHeight?: number; + chain: string; + network: string; + initialSyncComplete: boolean; + }) { + const { chain, network, initialSyncComplete, txs } = params; + if (!initialSyncComplete) { + return; + } + for (const tx of txs) { + await this.collection.update( + { + chain, + network, + from: tx.from, + nonce: tx.nonce, + txid: { $ne: tx.txid }, + blockHeight: SpentHeightIndicators.pending + }, + { $set: { blockHeight: SpentHeightIndicators.conflicting } }, + { w: 0, j: false, multi: true } + ); + } + return; + } + + getTransactions(params: { query: any; options: StreamingFindOptions }) { + let originalQuery = params.query; + const { query, options } = Storage.getFindOptions(this, params.options); + const finalQuery = Object.assign({}, originalQuery, query); + return this.collection.find(finalQuery, options).addCursorFlag('noCursorTimeout', true); + } + + abiDecode(input: string) { + try { + const erc20Data = getErc20Decoder().decodeMethod(input); + if (erc20Data) { + return { + type: 'ERC20', + ...erc20Data + }; + } + } catch (e) {} + try { + const erc721Data = getErc721Decoder().decodeMethod(input); + if (erc721Data) { + return { + type: 'ERC721', + ...erc721Data + }; + } + } catch (e) {} + try { + const invoiceData = getInvoiceDecoder().decodeMethod(input); + if (invoiceData) { + return { + type: 'INVOICE', + ...invoiceData + }; + } + } catch (e) {} + try { + const multisigData = getMultisigDecoder().decodeMethod(input); + if (multisigData) { + return { + type: 'MULTISIG', + ...multisigData + }; + } + } catch (e) {} + return undefined; + } + + _apiTransform( + tx: IRskTransaction | Partial>, + options?: TransformOptions + ): RskTransactionJSON | string { + const dataStr = `0x${tx.data!.toString('hex')}`; + const decodedData = this.abiDecode(dataStr); + + const transaction: RskTransactionJSON = { + txid: tx.txid || '', + network: tx.network || '', + chain: tx.chain || '', + blockHeight: valueOrDefault(tx.blockHeight, -1), + blockHash: tx.blockHash || '', + blockTime: tx.blockTime ? tx.blockTime.toISOString() : '', + blockTimeNormalized: tx.blockTimeNormalized ? tx.blockTimeNormalized.toISOString() : '', + fee: valueOrDefault(tx.fee, -1), + value: valueOrDefault(tx.value, -1), + data: dataStr, + gasLimit: valueOrDefault(tx.gasLimit, -1), + gasPrice: valueOrDefault(tx.gasPrice, -1), + nonce: valueOrDefault(tx.nonce, 0), + to: tx.to || '', + from: tx.from || '', + abiType: tx.abiType, + internal: tx.internal + ? tx.internal.map(t => ({ ...t, decodedData: this.abiDecode(t.action.input || '0x') })) + : [], + decodedData: valueOrDefault(decodedData, undefined), + receipt: valueOrDefault(tx.receipt, undefined) + }; + if (options && options.object) { + return transaction; + } + return JSON.stringify(transaction); + } +} +export let RskTransactionStorage = new RskTransactionModel(); diff --git a/packages/bitcore-node/src/modules/rsk/p2p/RskVerificationPeer.ts b/packages/bitcore-node/src/modules/rsk/p2p/RskVerificationPeer.ts new file mode 100644 index 00000000000..a1da907a7bb --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/p2p/RskVerificationPeer.ts @@ -0,0 +1,217 @@ +import logger from '../../../logger'; +import { ITransaction } from '../../../models/baseTransaction'; +import { ErrorType, IVerificationPeer } from '../../../services/verification'; +import { RskBlockStorage } from '../models/block'; +import { RskP2pWorker } from './p2p'; + +export class RskVerificationPeer extends RskP2pWorker implements IVerificationPeer { + prevBlockNum = 0; + prevHash = ''; + nextBlockHash = ''; + deepScan = false; + + enableDeepScan() { + this.deepScan = true; + } + + disableDeepScan() { + this.deepScan = false; + } + + async setupListeners() { + this.txSubscription = await this.web3!.eth.subscribe('pendingTransactions'); + this.txSubscription.subscribe((_err, tx) => { + this.events.emit('transaction', tx); + }); + this.blockSubscription = await this.web3!.eth.subscribe('newBlockHeaders'); + this.blockSubscription.subscribe((_err, block) => { + this.events.emit('block', block); + }); + } + + async resync(start: number, end: number) { + const { chain, network } = this; + let currentHeight = Math.max(1, start); + while (currentHeight <= end) { + let lastLog = Date.now(); + const block = await this.getBlock(currentHeight); + const { convertedBlock, convertedTxs } = await this.convertBlock(block); + + const nextBlock = await RskBlockStorage.collection.findOne({ chain, network, previousBlockHash: block.hash }); + if (nextBlock) { + convertedBlock.nextBlockHash = nextBlock.hash; + } + + await this.blockModel.processBlock({ + chain: this.chain, + network: this.network, + forkHeight: this.chainConfig.forkHeight, + parentChain: this.chainConfig.parentChain, + initialSyncComplete: this.initialSyncComplete, + block: convertedBlock, + transactions: convertedTxs + }); + + currentHeight++; + + if (Date.now() - lastLog > 100) { + logger.info('Re-Sync ', { + chain, + network, + height: currentHeight + }); + lastLog = Date.now(); + } + } + } + + async getBlockForNumber(blockNum: number) { + return this.getBlock(blockNum); + } + + async validateDataForBlock(blockNum: number, tipHeight: number, log = false) { + let success = true; + const { chain, network } = this; + const atTipOfChain = blockNum === tipHeight; + const errors = new Array(); + + const [block, blockTxs] = await Promise.all([ + this.blockModel.collection.findOne({ + chain, + network, + height: blockNum, + processed: true + }), + this.txModel.collection.find({ chain, network, blockHeight: blockNum }).toArray() + ]); + + if (!block) { + success = false; + const error = { + model: 'block', + err: true, + type: 'MISSING_BLOCK', + payload: { blockNum } + }; + errors.push(error); + if (log) { + console.log(JSON.stringify(error)); + } + return { success, errors }; + } + + const blockTxids = blockTxs.map(t => t.txid); + const firstHash = blockTxs[0] ? blockTxs[0].blockHash : block!.hash; + const [mempoolTxs, blocksForHash, blocksForHeight] = await Promise.all([ + this.txModel.collection.find({ chain, network, blockHeight: -1, txid: { $in: blockTxids } }).toArray(), + this.blockModel.collection.countDocuments({ chain, network, hash: firstHash }), + this.blockModel.collection.countDocuments({ + chain, + network, + height: blockNum, + processed: true + }) + ]); + + const seenTxs = {} as { [txid: string]: ITransaction }; + + const linearProgress = this.prevBlockNum && this.prevBlockNum == blockNum - 1; + const prevHashMismatch = this.prevHash && block.previousBlockHash != this.prevHash; + const nextHashMismatch = this.nextBlockHash && block.hash != this.nextBlockHash; + this.prevHash = block.hash; + this.nextBlockHash = block.nextBlockHash; + this.prevBlockNum = blockNum; + const missingLinearData = linearProgress && (prevHashMismatch || nextHashMismatch); + const missingNextBlockHash = !atTipOfChain && !block.nextBlockHash; + const missingPrevBlockHash = !block.previousBlockHash; + const missingData = missingNextBlockHash || missingPrevBlockHash || missingLinearData; + + if (!block || block.transactionCount != blockTxs.length || missingData) { + success = false; + const error = { + model: 'block', + err: true, + type: 'CORRUPTED_BLOCK', + payload: { blockNum, txCount: block.transactionCount, foundTxs: blockTxs.length } + }; + + errors.push(error); + + if (log) { + console.log(JSON.stringify(error)); + } + } + + for (let tx of mempoolTxs) { + success = false; + const error = { model: 'transaction', err: true, type: 'DUPE_TRANSACTION', payload: { tx, blockNum } }; + errors.push(error); + if (log) { + console.log(JSON.stringify(error)); + } + } + + for (let tx of blockTxs) { + if (tx.fee < 0) { + success = false; + const error = { model: 'transaction', err: true, type: 'NEG_FEE', payload: { tx, blockNum } }; + errors.push(error); + if (log) { + console.log(JSON.stringify(error)); + } + } + if (seenTxs[tx.txid]) { + success = false; + const error = { model: 'transaction', err: true, type: 'DUPE_TRANSACTION', payload: { tx, blockNum } }; + errors.push(error); + if (log) { + console.log(JSON.stringify(error)); + } + } else { + seenTxs[tx.txid] = tx; + } + } + + if (blocksForHeight === 0) { + success = false; + const error = { + model: 'block', + err: true, + type: 'MISSING_BLOCK', + payload: { blockNum } + }; + errors.push(error); + if (log) { + console.log(JSON.stringify(error)); + } + } + + if (blocksForHeight > 1) { + success = false; + const error = { + model: 'block', + err: true, + type: 'DUPE_BLOCKHEIGHT', + payload: { blockNum, blocksForHeight } + }; + errors.push(error); + if (log) { + console.log(JSON.stringify(error)); + } + } + // blocks with same hash + if (blockTxs.length > 0) { + const hashFromTx = blockTxs[0].blockHash; + if (blocksForHash > 1) { + success = false; + const error = { model: 'block', err: true, type: 'DUPE_BLOCKHASH', payload: { hash: hashFromTx, blockNum } }; + errors.push(error); + if (log) { + console.log(JSON.stringify(error)); + } + } + } + + return { success, errors }; + } +} diff --git a/packages/bitcore-node/src/modules/rsk/p2p/p2p.ts b/packages/bitcore-node/src/modules/rsk/p2p/p2p.ts new file mode 100644 index 00000000000..e11749c6be3 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/p2p/p2p.ts @@ -0,0 +1,400 @@ +import { EventEmitter } from 'events'; +import Web3 from 'web3'; +import { timestamp } from '../../../logger'; +import logger from '../../../logger'; +import { StateStorage } from '../../../models/state'; +import { ChainStateProvider } from '../../../providers/chain-state'; +import { BaseP2PWorker } from '../../../services/p2p'; +import { valueOrDefault } from '../../../utils/check'; +import { wait } from '../../../utils/wait'; +import { RSKStateProvider } from '../api/csp'; +import { RskBlockModel, RskBlockStorage } from '../models/block'; +import { RskTransactionModel, RskTransactionStorage } from '../models/transaction'; +import { IRskBlock, IRskTransaction, ParityBlock, ParityTransaction } from '../types'; +import { ParityRPC } from './parityRpc'; + +export class RskP2pWorker extends BaseP2PWorker { + protected chainConfig: any; + protected syncing: boolean; + protected initialSyncComplete: boolean; + protected blockModel: RskBlockModel; + protected txModel: RskTransactionModel; + protected txSubscription: any; + protected blockSubscription: any; + protected rpc?: ParityRPC; + protected provider: RSKStateProvider; + protected web3?: Web3; + protected web3Sockets?: Web3; + protected invCache: any; + protected invCacheLimits: any; + public events: EventEmitter; + public disconnecting: boolean; + + constructor({ chain, network, chainConfig, blockModel = RskBlockStorage, txModel = RskTransactionStorage }) { + super({ chain, network, chainConfig, blockModel }); + this.chain = chain || 'RSK'; + this.network = network; + this.chainConfig = chainConfig; + this.syncing = false; + this.initialSyncComplete = false; + this.blockModel = blockModel; + this.txModel = txModel; + this.provider = new RSKStateProvider(); + this.events = new EventEmitter(); + this.invCache = {}; + this.invCacheLimits = { + TX: 100000 + }; + this.disconnecting = false; + } + + cacheInv(type: 'TX', hash: string): void { + if (!this.invCache[type]) { + this.invCache[type] = []; + } + if (this.invCache[type].length > this.invCacheLimits[type]) { + this.invCache[type].shift(); + } + this.invCache[type].push(hash); + } + + isCachedInv(type: 'TX', hash: string): boolean { + if (!this.invCache[type]) { + this.invCache[type] = []; + } + return this.invCache[type].includes(hash); + } + + async setupListeners() { + const { host, port, protocol } = this.chainConfig.provider; + this.events.on('disconnected', async () => { + logger.warn( + `${timestamp()} | Not connected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}` + ); + }); + this.events.on('connected', async () => { + if (this.web3Sockets == null && (protocol === 'ws' || protocol === 'wss')) { + const wsProvider = new Web3.providers.WebsocketProvider(protocol + '://' + host + ':' + port); + this.web3Sockets = new Web3(wsProvider); + } + this.txSubscription = await this.web3Sockets!.eth.subscribe('pendingTransactions'); + this.txSubscription.subscribe(async (_err, txid) => { + if (!this.isCachedInv('TX', txid)) { + this.cacheInv('TX', txid); + if (txid != null && txid != false) { + const tx = (await this.web3!.eth.getTransaction(txid)) as ParityTransaction; + if (tx) { + await this.processTransaction(tx); + this.events.emit('transaction', tx); + } + } + } + }); + this.blockSubscription = await this.web3Sockets!.eth.subscribe('newBlockHeaders'); + this.blockSubscription.subscribe((_err, block) => { + this.events.emit('block', block); + if (!this.syncing) { + this.sync(); + } + }); + }); + } + + async disconnect() { + this.disconnecting = true; + try { + if (this.txSubscription) { + this.txSubscription.unsubscribe(); + } + if (this.blockSubscription) { + this.blockSubscription.unsubscribe(); + } + } catch (e) { + console.error(e); + } + } + + async getWeb3() { + return this.provider.getWeb3(this.network); + } + + async handleReconnects() { + this.disconnecting = false; + let firstConnect = true; + let connected = false; + let disconnected = false; + const { host, port } = this.chainConfig.provider; + while (!this.disconnecting && !this.stopping) { + try { + if (!this.web3) { + const { web3 } = await this.getWeb3(); + this.web3 = web3; + this.rpc = new ParityRPC(this.web3); + } + try { + connected = await this.web3.eth.net.isListening(); + } catch (e) { + connected = false; + } + if (connected) { + if (disconnected || firstConnect) { + this.events.emit('connected'); + } + } else { + const { web3 } = await this.getWeb3(); + this.web3 = web3; + this.rpc = new ParityRPC(this.web3); + this.events.emit('disconnected'); + } + if (disconnected && connected && !firstConnect) { + logger.warn( + `${timestamp()} | Reconnected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}` + ); + } + if (connected && firstConnect) { + firstConnect = false; + logger.info( + `${timestamp()} | Connected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}` + ); + } + disconnected = !connected; + } catch (e) {} + await wait(2000); + } + } + + async connect() { + this.handleReconnects(); + } + + public async getBlock(height: number) { + return (this.rpc!.getBlock(height) as unknown) as ParityBlock; + } + + async processBlock(block: IRskBlock, transactions: IRskTransaction[]): Promise { + await this.blockModel.addBlock({ + chain: this.chain, + network: this.network, + forkHeight: this.chainConfig.forkHeight, + parentChain: this.chainConfig.parentChain, + initialSyncComplete: this.initialSyncComplete, + block, + transactions + }); + if (!this.syncing) { + logger.info(`Added block ${block.hash}`, { + chain: this.chain, + network: this.network + }); + } + } + + async processTransaction(tx: ParityTransaction) { + const now = new Date(); + const convertedTx = this.convertTx(tx); + this.txModel.batchImport({ + chain: this.chain, + network: this.network, + txs: [convertedTx], + height: -1, + mempoolTime: now, + blockTime: now, + blockTimeNormalized: now, + initialSyncComplete: true + }); + } + + async sync() { + if (this.syncing) { + return false; + } + const { chain, chainConfig, network } = this; + const { parentChain, forkHeight } = chainConfig; + this.syncing = true; + const state = await StateStorage.collection.findOne({}); + this.initialSyncComplete = + state && state.initialSyncComplete && state.initialSyncComplete.includes(`${chain}:${network}`); + let tip = await ChainStateProvider.getLocalTip({ chain, network }); + if (parentChain && (!tip || tip.height < forkHeight)) { + let parentTip = await ChainStateProvider.getLocalTip({ chain: parentChain, network }); + while (!parentTip || parentTip.height < forkHeight) { + logger.info(`Waiting until ${parentChain} syncs before ${chain} ${network}`); + await new Promise(resolve => { + setTimeout(resolve, 5000); + }); + parentTip = await ChainStateProvider.getLocalTip({ chain: parentChain, network }); + } + } + + const startHeight = tip ? tip.height : 0; + const startTime = Date.now(); + try { + let bestBlock = await this.web3!.eth.getBlockNumber(); + let lastLog = 0; + let currentHeight = tip ? tip.height : 0; + logger.info(`Syncing ${bestBlock - currentHeight} blocks for ${chain} ${network}`); + while (currentHeight <= bestBlock) { + const block = await this.getBlock(currentHeight); + if (!block) { + await wait(1000); + continue; + } + const { convertedBlock, convertedTxs } = await this.convertBlock(block); + await this.processBlock(convertedBlock, convertedTxs); + if (currentHeight === bestBlock) { + bestBlock = await this.web3!.eth.getBlockNumber(); + } + tip = await ChainStateProvider.getLocalTip({ chain, network }); + currentHeight = tip ? tip.height + 1 : 0; + + const oneSecond = 1000; + const now = Date.now(); + if (now - lastLog > oneSecond) { + const blocksProcessed = currentHeight - startHeight; + const elapsedMinutes = (now - startTime) / (60 * oneSecond); + logger.info( + `${timestamp()} | Syncing... | Chain: ${chain} | Network: ${network} |${(blocksProcessed / elapsedMinutes) + .toFixed(2) + .padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}` + ); + lastLog = Date.now(); + } + } + } catch (err) { + logger.error(`Error syncing ${chain} ${network}`, err.message); + await wait(2000); + this.syncing = false; + return this.sync(); + } + logger.info(`${chain}:${network} up to date.`); + this.syncing = false; + StateStorage.collection.findOneAndUpdate( + {}, + { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, + { upsert: true } + ); + this.events.emit('SYNCDONE'); + return true; + } + + async syncDone() { + return new Promise(resolve => this.events.once('SYNCDONE', resolve)); + } + + async convertBlock(block: ParityBlock) { + // console.log('converting block'); + const blockTime = Number(block.timestamp) * 1000; + const hash = block.hash; + const height = block.number; + + const convertedBlock: IRskBlock = { + chain: this.chain, + network: this.network, + height, + hash, + coinbase: Buffer.from(block.miner), + merkleRoot: Buffer.from(block.transactionsRoot), + time: new Date(blockTime), + timeNormalized: new Date(blockTime), + nonce: Buffer.from(block.extraData), + previousBlockHash: block.parentHash, + difficulty: block.difficulty, + totalDifficulty: block.totalDifficulty, + nextBlockHash: '', + transactionCount: block.transactions.length, + size: block.size, + logsBloom: Buffer.from(block.logsBloom), + sha3Uncles: Buffer.from(block.sha3Uncles), + receiptsRoot: Buffer.from(block.receiptsRoot), + processed: false, + gasLimit: block.gasLimit, + gasUsed: block.gasUsed, + stateRoot: Buffer.from(block.stateRoot), + reward: 0 + }; + const transactions = block.transactions as Array; + const convertedTxs = transactions.map(t => this.convertTx(t, convertedBlock)); + const internalTxs = await this.rpc!.getTransactionsFromBlock(convertedBlock.height); + for (const tx of internalTxs) { + if (tx && tx.action) { + const foundIndex = convertedTxs.findIndex( + t => + t.txid === tx.transactionHash && + t.from !== tx.action.from && + t.to.toLowerCase() !== (tx.action.to || '').toLowerCase() + ); + if (foundIndex > -1) { + convertedTxs[foundIndex].internal.push(tx); + } + if (tx.error) { + const errorIndex = convertedTxs.findIndex(t => t.txid === tx.transactionHash); + if (errorIndex && errorIndex > -1) { + convertedTxs[errorIndex].error = tx.error; + } + } + } + } + + return { convertedBlock, convertedTxs }; + } + + convertTx(tx: Partial, block?: IRskBlock): IRskTransaction { + if (!block) { + const txid = tx.hash || ''; + const to = tx.to || ''; + const from = tx.from || ''; + const value = Number(tx.value); + const fee = Number(tx.gas) * Number(tx.gasPrice); + const abiType = this.txModel.abiDecode(tx.input!); + const nonce = tx.nonce || 0; + const convertedTx: IRskTransaction = { + chain: this.chain, + network: this.network, + blockHeight: valueOrDefault(tx.blockNumber, -1), + blockHash: valueOrDefault(tx.blockHash, undefined), + data: Buffer.from(tx.input || '0x'), + txid, + blockTime: new Date(), + blockTimeNormalized: new Date(), + fee, + transactionIndex: tx.transactionIndex || 0, + value, + wallets: [], + to, + from, + gasLimit: Number(tx.gas), + gasPrice: Number(tx.gasPrice), + // gasUsed: Number(tx.gasUsed), + nonce, + internal: [] + }; + if (abiType) { + convertedTx.abiType = abiType; + } + return convertedTx; + } else { + const { hash: blockHash, time: blockTime, timeNormalized: blockTimeNormalized, height } = block; + const noBlockTx = this.convertTx(tx); + return { + ...noBlockTx, + blockHeight: height, + blockHash, + blockTime, + blockTimeNormalized + }; + } + } + + async stop() { + this.stopping = true; + logger.debug(`Stopping worker for chain ${this.chain} ${this.network}`); + await this.disconnect(); + } + + async start() { + logger.debug(`Started worker for chain ${this.chain} ${this.network}`); + this.connect(); + this.setupListeners(); + this.sync(); + } +} diff --git a/packages/bitcore-node/src/modules/rsk/p2p/parityRpc.ts b/packages/bitcore-node/src/modules/rsk/p2p/parityRpc.ts new file mode 100644 index 00000000000..f0dd8bb7757 --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/p2p/parityRpc.ts @@ -0,0 +1,113 @@ +import AbiDecoder from 'abi-decoder'; +import Web3 from 'web3'; +import { LoggifyClass } from '../../../decorators/Loggify'; +import { ERC20Abi } from '../abi/erc20'; +import { ERC721Abi } from '../abi/erc721'; +import { RskTransactionStorage } from '../models/transaction'; +import { IRskTransaction } from '../types'; + +AbiDecoder.addABI(ERC20Abi); +AbiDecoder.addABI(ERC721Abi); + +if (Symbol['asyncIterator'] === undefined) (Symbol as any)['asyncIterator'] = Symbol.for('asyncIterator'); + +interface ParityCall { + callType?: 'call' | 'delegatecall'; + author?: string; + rewardType?: 'block' | 'uncle'; + from?: string; + gas?: string; + input?: string; + to: string; + value: string; +} + +export interface ParityTraceResponse { + action: ParityCall; + blockHash: string; + blockNumber: number; + error: string; + result: { gasUsed?: string; output: string }; + subtraces: number; + traceAddress: []; + transactionHash: string; + transactionPosition: number; + type: 'reward' | 'call' | 'delegatecall' | 'create'; +} + +export interface ClassifiedTrace extends ParityTraceResponse { + abiType?: IRskTransaction['abiType']; + to?: string; +} + +export interface TokenTransferResponse { + name?: 'transfer'; + params?: Array<{ name: string; value: string; type: string }>; +} + +interface Callback { + (error: Error): void; + (error: null, val: ResultType): void; +} + +interface JsonRPCRequest { + jsonrpc: string; + method: string; + params: any[]; + id: number; +} +interface JsonRPCResponse { + jsonrpc: string; + id: number; + result?: any; + error?: string; +} + +@LoggifyClass +export class ParityRPC { + web3: Web3; + + constructor(web3: Web3) { + this.web3 = web3; + } + + public getBlock(blockNumber: number) { + return this.web3.eth.getBlock(blockNumber, true); + } + + private async traceBlock(blockNumber: number) { + const txs = await this.send>({ + method: 'trace_block', + params: [this.web3.utils.toHex(blockNumber)], + jsonrpc: '2.0', + id: 1 + }); + return txs; + } + + public async getTransactionsFromBlock(blockNumber: number) { + const txs = (await this.traceBlock(blockNumber)) || []; + return txs.map(tx => this.transactionFromParityTrace(tx)); + } + + public send(data: JsonRPCRequest) { + return new Promise((resolve, reject) => { + const provider = this.web3.eth.currentProvider as any; // Import type HttpProvider web3-core + provider.send(data, function(err, data) { + if (err) return reject(err); + resolve(data.result as T); + } as Callback); + }); + } + + private transactionFromParityTrace(tx: ParityTraceResponse): ClassifiedTrace { + const abiType = RskTransactionStorage.abiDecode(tx.action.input!); + const convertedTx: ClassifiedTrace = { + ...tx + }; + if (abiType) { + convertedTx.abiType = abiType; + } + return convertedTx; + } +} diff --git a/packages/bitcore-node/src/modules/rsk/types.ts b/packages/bitcore-node/src/modules/rsk/types.ts new file mode 100644 index 00000000000..9825711fbed --- /dev/null +++ b/packages/bitcore-node/src/modules/rsk/types.ts @@ -0,0 +1,182 @@ +import BN from 'bn.js'; + +import { ITransaction } from '../../models/baseTransaction'; +import { IBlock } from '../../types/Block'; +import { ClassifiedTrace, TokenTransferResponse } from './p2p/parityRpc'; + +export interface ParityBlock { + difficulty: string; + extraData: string; + gasLimit: number; + gasUsed: number; + hash: string; + logsBloom: string; + miner: string; + mixHash: string; + nonce: string; + number: number; + parentHash: string; + receiptsRoot: string; + sha3Uncles: string; + size: number; + stateRoot: string; + timestamp: number; + totalDifficulty: string; + transactions: Array; + transactionsRoot: string; + uncles: Array; + minimumGasPrice: number; + bitcoinMergedMiningHeader: string; + bitcoinMergedMiningCoinbaseTransaction: string; + bitcoinMergedMiningMerkleProof: string; + hashForMergedMining: string; +} +export interface ParityTransaction { + blockHash: string; + blockNumber: number; + chainId: number; + condition: number; + creates: number; + from: string; + gas: number; + gasPrice: string; + hash: string; + input: string; + nonce: number; + publicKey: string; + r: string; + raw: string; + s: string; + standardV: string; + to: string; + transactionIndex: number; + v: string; + value: string; +} + +export type Networks = 'mainnet' | 'testnet'; + +export interface RskBlock { + header: RskHeader; + transactions: Transaction[]; + uncleHeaders: RskHeader[]; + raw: Buffer[]; + txTrie: any; +} + +export interface RskHeader { + parentHash: Buffer; + uncleHash: Buffer; + coinbase: Buffer; + stateRoot: Buffer; + transactionsTrie: Buffer; + receiptTrie: Buffer; + bloom: Buffer; + difficulty: Buffer; + number: Buffer; + gasLimit: Buffer; + gasUsed: Buffer; + timestamp: Buffer; + extraData: Buffer; + mixHash: Buffer; + nonce: Buffer; + raw: Array; + hash: () => Buffer; +} + +export interface Transaction { + hash: () => Buffer; + nonce: Buffer; + gasPrice: Buffer; + gasLimit: Buffer; + to: Buffer; + from: Buffer; + value: Buffer; + data: Buffer; + // EIP 155 chainId - mainnet: 30, testnet: 31 + chainId: number; + getUpfrontCost: () => BN; +} + +export type IRskBlock = IBlock & { + coinbase: Buffer; + nonce: Buffer; + gasLimit: number; + gasUsed: number; + stateRoot: Buffer; + logsBloom: Buffer; + sha3Uncles: Buffer; + receiptsRoot: Buffer; + merkleRoot: Buffer; + difficulty: string; + totalDifficulty: string; + reward: number; +}; + +export type IRskTransaction = ITransaction & { + data: Buffer; + gasLimit: number; + gasPrice: number; + nonce: number; + to: string; + from: string; + internal: Array; + transactionIndex: number; + abiType?: { + type: string; + name: string; + params: Array<{ name: string; value: string; type: string }>; + }; + error?: string; + receipt?: { + status: boolean; + transactionHash: string; + transactionIndex: number; + blockHash: string; + blockNumber: number; + contractAddress?: string; + cumulativeGasUsed: number; + gasUsed: number; + logs: Array; + }; +}; + +export interface TransactionJSON { + txid: string; + chain: string; + network: string; + blockHeight: number; + blockHash?: string; + blockTime: string; + blockTimeNormalized: string; + fee: number; + size: number; + value: number; +} + +export interface AbiDecodedData { + type: string; + decodedData: TokenTransferResponse; +} +export type DecodedTrace = ClassifiedTrace & AbiDecodedData; +export interface RskTransactionJSON { + txid: string; + chain: string; + network: string; + blockHeight: number; + blockHash: string; + blockTime: string; + blockTimeNormalized: string; + fee: number; + value: number; + gasLimit: number; + gasPrice: number; + nonce: number; + to: string; + from: string; + abiType?: IRskTransaction['abiType']; + decodedData?: AbiDecodedData; + data: string; + internal: Array; + receipt?: IRskTransaction['receipt']; +} diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index fed6d5ff51c..4860914461b 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -2525,7 +2525,10 @@ export class API extends EventEmitter { // * @return {Callback} cb - Return error (if exists) and nonce // */ getNonce(opts, cb) { - $.checkArgument(opts.coin == 'eth', 'Invalid coin: must be "eth"'); + $.checkArgument( + opts.coin == 'eth' || opts.coin == 'rsk' || opts.coin == 'rbtc', + 'Invalid coin: must be "eth" or "rsk"' + ); var qs = []; qs.push(`coin=${opts.coin}`); @@ -2943,6 +2946,8 @@ export class API extends EventEmitter { ['doge', 'testnet'], ['ltc', 'testnet'], ['ltc', 'livenet'], + ['rsk', 'livenet'], + ['rbtc', 'livenet'], ['btc', 'livenet', true], ['bch', 'livenet', true], ['doge', 'livenet', true], diff --git a/packages/bitcore-wallet-client/src/lib/common/utils.ts b/packages/bitcore-wallet-client/src/lib/common/utils.ts index 50fc59945be..4a3406da990 100644 --- a/packages/bitcore-wallet-client/src/lib/common/utils.ts +++ b/packages/bitcore-wallet-client/src/lib/common/utils.ts @@ -24,7 +24,8 @@ const Bitcore_ = { eth: Bitcore, xrp: Bitcore, doge: BitcoreLibDoge, - ltc: BitcoreLibLtc + ltc: BitcoreLibLtc, + rsk: Bitcore }; const PrivateKey = Bitcore.PrivateKey; const PublicKey = Bitcore.PublicKey; diff --git a/packages/bitcore-wallet-client/src/lib/key.ts b/packages/bitcore-wallet-client/src/lib/key.ts index 1f4062ec03a..9f532f5a30b 100644 --- a/packages/bitcore-wallet-client/src/lib/key.ts +++ b/packages/bitcore-wallet-client/src/lib/key.ts @@ -393,6 +393,8 @@ export class Key { coinCode = '0'; } else if (opts.coin == 'eth') { coinCode = '60'; + } else if (opts.coin == 'rsk' || opts.coin == 'rbtc') { + coinCode = '137'; } else if (opts.coin == 'xrp') { coinCode = '144'; } else if (opts.coin == 'doge') { diff --git a/packages/bitcore-wallet-service/src/config.ts b/packages/bitcore-wallet-service/src/config.ts index c21c2449b9e..e7628c7cc1f 100644 --- a/packages/bitcore-wallet-service/src/config.ts +++ b/packages/bitcore-wallet-service/src/config.ts @@ -61,6 +61,14 @@ const Config = () => { url: 'https://api-eth.bitcore.io' } }, + rsk: { + livenet: { + url: 'https://api-rsk.bitcore.io' + }, + testnet: { + url: 'https://api-rsk.bitcore.io' + } + }, xrp: { livenet: { url: 'https://api-xrp.bitcore.io' diff --git a/packages/bitcore-wallet-service/src/lib/blockchainexplorer.ts b/packages/bitcore-wallet-service/src/lib/blockchainexplorer.ts index a6f0223109e..881751cd231 100644 --- a/packages/bitcore-wallet-service/src/lib/blockchainexplorer.ts +++ b/packages/bitcore-wallet-service/src/lib/blockchainexplorer.ts @@ -19,6 +19,10 @@ const PROVIDERS = { livenet: 'https://api-eth.bitcore.io', testnet: 'https://api-eth.bitcore.io' }, + rsk: { + livenet: 'https://api-rsk.bitcore.io', + testnet: 'https://api-rsk.bitcore.io' + }, xrp: { livenet: 'https://api-xrp.bitcore.io', testnet: 'https://api-xrp.bitcore.io' diff --git a/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts b/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts index 9387b321dae..93993efc0a3 100644 --- a/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts +++ b/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts @@ -13,6 +13,7 @@ const Bitcore_ = { btc: Bitcore, bch: require('bitcore-lib-cash'), eth: Bitcore, + rsk: Bitcore, xrp: Bitcore, doge: require('bitcore-lib-doge'), ltc: require('bitcore-lib-ltc') diff --git a/packages/bitcore-wallet-service/src/lib/blockchainmonitor.ts b/packages/bitcore-wallet-service/src/lib/blockchainmonitor.ts index bbbaacb39ab..bc9e72c6662 100644 --- a/packages/bitcore-wallet-service/src/lib/blockchainmonitor.ts +++ b/packages/bitcore-wallet-service/src/lib/blockchainmonitor.ts @@ -53,7 +53,8 @@ export class BlockchainMonitor { eth: {}, xrp: {}, doge: {}, - ltc: {} + ltc: {}, + rsk: {} }; const coinNetworkPairs = []; @@ -207,7 +208,7 @@ export class BlockchainMonitor { if (!out || !out.address || out.address.length < 10) return; // For eth, amount = 0 is ok, repeating addr payments are ok (no change). - if (coin != 'eth') { + if (coin != 'eth' && coin != 'rsk' && coin != 'rbtc') { if (!(out.amount > 0)) return; if (this.last.indexOf(out.address) >= 0) { logger.debug('The incoming tx"s out ' + out.address + ' was already processed'); @@ -215,7 +216,7 @@ export class BlockchainMonitor { } this.last[this.Ni++] = out.address; if (this.Ni >= this.N) this.Ni = 0; - } else if (coin == 'eth') { + } else if (coin == 'eth' || coin == 'rsk' || coin == 'rbtc') { if (this.lastTx.indexOf(data.txid) >= 0) { logger.debug('The incoming tx ' + data.txid + ' was already processed'); return; diff --git a/packages/bitcore-wallet-service/src/lib/chain/index.ts b/packages/bitcore-wallet-service/src/lib/chain/index.ts index 1a4ad549458..a465706f547 100644 --- a/packages/bitcore-wallet-service/src/lib/chain/index.ts +++ b/packages/bitcore-wallet-service/src/lib/chain/index.ts @@ -5,6 +5,7 @@ import { BtcChain } from './btc'; import { DogeChain } from './doge'; import { EthChain } from './eth'; import { LtcChain } from './ltc'; +import { RskChain } from './rsk'; import { XrpChain } from './xrp'; const Common = require('../common'); @@ -72,7 +73,8 @@ const chain: { [chain: string]: IChain } = { ETH: new EthChain(), XRP: new XrpChain(), DOGE: new DogeChain(), - LTC: new LtcChain() + LTC: new LtcChain(), + RSK: new RskChain() }; class ChainProxy { @@ -84,7 +86,7 @@ class ChainProxy { getChain(coin: string): string { let normalizedChain = coin.toUpperCase(); if (Constants.ERC20[normalizedChain]) { - normalizedChain = 'ETH'; + normalizedChain = 'ETH'; // TODO: add RSK } return normalizedChain; } diff --git a/packages/bitcore-wallet-service/src/lib/chain/rsk/abi-erc20.ts b/packages/bitcore-wallet-service/src/lib/chain/rsk/abi-erc20.ts new file mode 100644 index 00000000000..6c8fba65dc8 --- /dev/null +++ b/packages/bitcore-wallet-service/src/lib/chain/rsk/abi-erc20.ts @@ -0,0 +1,222 @@ +export const ERC20Abi = [ + { + constant: true, + inputs: [], + name: 'name', + outputs: [ + { + name: '', + type: 'string' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'approve', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: '_from', + type: 'address' + }, + { + name: '_to', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'transferFrom', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'decimals', + outputs: [ + { + name: '', + type: 'uint8' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + name: 'balance', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'symbol', + outputs: [ + { + name: '', + type: 'string' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: '_to', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'transfer', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address' + }, + { + name: '_spender', + type: 'address' + } + ], + name: 'allowance', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + payable: true, + stateMutability: 'payable', + type: 'fallback' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'owner', + type: 'address' + }, + { + indexed: true, + name: 'spender', + type: 'address' + }, + { + indexed: false, + name: 'value', + type: 'uint256' + } + ], + name: 'Approval', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'from', + type: 'address' + }, + { + indexed: true, + name: 'to', + type: 'address' + }, + { + indexed: false, + name: 'value', + type: 'uint256' + } + ], + name: 'Transfer', + type: 'event' + } +]; diff --git a/packages/bitcore-wallet-service/src/lib/chain/rsk/abi-invoice.ts b/packages/bitcore-wallet-service/src/lib/chain/rsk/abi-invoice.ts new file mode 100644 index 00000000000..34040f619a6 --- /dev/null +++ b/packages/bitcore-wallet-service/src/lib/chain/rsk/abi-invoice.ts @@ -0,0 +1,277 @@ +export const InvoiceAbi = [ + { + constant: true, + inputs: [], + name: 'owner', + outputs: [ + { + name: '', + type: 'address' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'quoteSigner', + outputs: [ + { + name: '', + type: 'address' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + name: '', + type: 'bytes32' + } + ], + name: 'isPaid', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + name: 'valueSigner', + type: 'address' + } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'constructor' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'hash', + type: 'bytes32' + }, + { + indexed: true, + name: 'tokenContract', + type: 'address' + }, + { + indexed: false, + name: 'time', + type: 'uint256' + }, + { + indexed: false, + name: 'value', + type: 'uint256' + } + ], + name: 'PaymentAccepted', + type: 'event' + }, + { + constant: true, + inputs: [ + { + name: 'value', + type: 'uint256' + }, + { + name: 'gasPrice', + type: 'uint256' + }, + { + name: 'expiration', + type: 'uint256' + }, + { + name: 'payload', + type: 'bytes32' + }, + { + name: 'hash', + type: 'bytes32' + }, + { + name: 'v', + type: 'uint8' + }, + { + name: 'r', + type: 'bytes32' + }, + { + name: 's', + type: 'bytes32' + }, + { + name: 'tokenContract', + type: 'address' + } + ], + name: 'isValidPayment', + outputs: [ + { + name: 'valid', + type: 'bool' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + name: 'value', + type: 'uint256' + }, + { + name: 'gasPrice', + type: 'uint256' + }, + { + name: 'expiration', + type: 'uint256' + }, + { + name: 'payload', + type: 'bytes32' + }, + { + name: 'hash', + type: 'bytes32' + }, + { + name: 'v', + type: 'uint8' + }, + { + name: 'r', + type: 'bytes32' + }, + { + name: 's', + type: 'bytes32' + }, + { + name: 'tokenContract', + type: 'address' + } + ], + name: 'validatePayment', + outputs: [ + { + name: 'valid', + type: 'bool' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: 'value', + type: 'uint256' + }, + { + name: 'gasPrice', + type: 'uint256' + }, + { + name: 'expiration', + type: 'uint256' + }, + { + name: 'payload', + type: 'bytes32' + }, + { + name: 'hash', + type: 'bytes32' + }, + { + name: 'v', + type: 'uint8' + }, + { + name: 'r', + type: 'bytes32' + }, + { + name: 's', + type: 'bytes32' + }, + { + name: 'tokenContract', + type: 'address' + } + ], + name: 'pay', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: 'tokenContract', + type: 'address' + } + ], + name: 'withdraw', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: 'newQuoteSigner', + type: 'address' + } + ], + name: 'setSigner', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: 'newAdmin', + type: 'address' + } + ], + name: 'setAdmin', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + } +]; diff --git a/packages/bitcore-wallet-service/src/lib/chain/rsk/index.ts b/packages/bitcore-wallet-service/src/lib/chain/rsk/index.ts new file mode 100644 index 00000000000..5642d5df94f --- /dev/null +++ b/packages/bitcore-wallet-service/src/lib/chain/rsk/index.ts @@ -0,0 +1,477 @@ +import { Transactions, Validation } from 'crypto-wallet-core'; +import { Web3 } from 'crypto-wallet-core'; +import _ from 'lodash'; +import { IAddress } from 'src/lib/model/address'; +import { IChain, INotificationData } from '..'; +import { ClientError } from '../../errors/clienterror'; +import logger from '../../logger'; +import { ERC20Abi } from './abi-erc20'; +import { InvoiceAbi } from './abi-invoice'; + +const Common = require('../../common'); +const Constants = Common.Constants; +const Defaults = Common.Defaults; +const Errors = require('../../errors/errordefinitions'); + +function requireUncached(module) { + delete require.cache[require.resolve(module)]; + return require(module); +} + +const Erc20Decoder = requireUncached('abi-decoder'); +Erc20Decoder.addABI(ERC20Abi); +function getErc20Decoder() { + return Erc20Decoder; +} + +const InvoiceDecoder = requireUncached('abi-decoder'); +InvoiceDecoder.addABI(InvoiceAbi); +function getInvoiceDecoder() { + return InvoiceDecoder; +} + +export class RskChain implements IChain { + /** + * Converts Bitcore Balance Response. + * @param {Object} bitcoreBalance - { unconfirmed, confirmed, balance } + * @param {Number} locked - Sum of txp.amount + * @returns {Object} balance - Total amount & locked amount. + */ + private convertBitcoreBalance(bitcoreBalance, locked) { + const { unconfirmed, confirmed, balance } = bitcoreBalance; + // we ASUME all locked as confirmed, for RSK. + const convertedBalance = { + totalAmount: balance, + totalConfirmedAmount: confirmed, + lockedAmount: locked, + lockedConfirmedAmount: locked, + availableAmount: balance - locked, + availableConfirmedAmount: confirmed - locked, + byAddress: [] + }; + return convertedBalance; + } + + getSizeSafetyMargin() { + return 0; + } + + getInputSizeSafetyMargin() { + return 0; + } + + notifyConfirmations() { + return false; + } + + supportsMultisig() { + return false; + } + + getWalletBalance(server, wallet, opts, cb) { + const bc = server._getBlockchainExplorer(wallet.coin, wallet.network); + + if (opts.tokenAddress) { + wallet.tokenAddress = opts.tokenAddress; + } + + if (opts.multisigContractAddress) { + wallet.multisigContractAddress = opts.multisigContractAddress; + opts.network = wallet.network; + } + + bc.getBalance(wallet, (err, balance) => { + if (err) { + return cb(err); + } + server.getPendingTxs(opts, (err, txps) => { + if (err) return cb(err); + // Do not lock rsk multisig amount + const lockedSum = opts.multisigContractAddress ? 0 : _.sumBy(txps, 'amount') || 0; + const convertedBalance = this.convertBitcoreBalance(balance, lockedSum); + server.storage.fetchAddresses(server.walletId, (err, addresses: IAddress[]) => { + if (err) return cb(err); + if (addresses.length > 0) { + const byAddress = [ + { + address: addresses[0].address, + path: addresses[0].path, + amount: convertedBalance.totalAmount + } + ]; + convertedBalance.byAddress = byAddress; + } + return cb(null, convertedBalance); + }); + }); + }); + } + + getWalletSendMaxInfo(server, wallet, opts, cb) { + server.getBalance({}, (err, balance) => { + if (err) return cb(err); + const { totalAmount, availableAmount } = balance; + let fee = opts.feePerKb * Defaults.MIN_GAS_LIMIT; + return cb(null, { + utxosBelowFee: 0, + amountBelowFee: 0, + amount: availableAmount - fee, + feePerKb: opts.feePerKb, + fee + }); + }); + } + + getDustAmountValue() { + return 0; + } + + getTransactionCount(server, wallet, from) { + return new Promise((resolve, reject) => { + server._getTransactionCount(wallet, from, (err, nonce) => { + if (err) return reject(err); + return resolve(nonce); + }); + }); + } + + getChangeAddress() {} + + checkDust(output, opts) {} + + getFee(server, wallet, opts) { + return new Promise(resolve => { + server._getFeePerKb(wallet, opts, async (err, inFeePerKb) => { + let feePerKb = inFeePerKb; + let gasPrice = inFeePerKb; + const { from } = opts; + const { coin, network } = wallet; + let inGasLimit; + let gasLimit; + const defaultGasLimit = opts.tokenAddress ? Defaults.DEFAULT_ERC20_GAS_LIMIT : Defaults.DEFAULT_GAS_LIMIT; + let fee = 0; + for (let output of opts.outputs) { + if (!output.gasLimit) { + try { + const to = opts.payProUrl + ? output.toAddress + : opts.tokenAddress + ? opts.tokenAddress + : opts.multisigContractAddress + ? opts.multisigContractAddress + : output.toAddress; + const value = opts.tokenAddress || opts.multisigContractAddress ? 0 : output.amount; + inGasLimit = await server.estimateGas({ + coin, + network, + from, + to, + value, + data: output.data, + gasPrice + }); + output.gasLimit = inGasLimit || defaultGasLimit; + } catch (err) { + output.gasLimit = defaultGasLimit; + } + } else { + inGasLimit = output.gasLimit; + } + if (_.isNumber(opts.fee)) { + // This is used for sendmax + gasPrice = feePerKb = Number((opts.fee / (inGasLimit || defaultGasLimit)).toFixed()); + } + gasLimit = inGasLimit || defaultGasLimit; + fee += feePerKb * gasLimit; + } + return resolve({ feePerKb, gasPrice, gasLimit, fee }); + }); + }); + } + + getBitcoreTx(txp, opts = { signed: true }) { + const { data, outputs, payProUrl, tokenAddress, multisigContractAddress, isTokenSwap } = txp; + const isERC20 = tokenAddress && !payProUrl && !isTokenSwap; + const isETHMULTISIG = multisigContractAddress; + const chain = isETHMULTISIG ? 'ETHMULTISIG' : isERC20 ? 'ERC20' : 'RSK'; + const recipients = outputs.map(output => { + return { + amount: output.amount, + address: output.toAddress, + data: output.data, + gasLimit: output.gasLimit + }; + }); + // Backwards compatibility BWC <= 8.9.0 + if (data) { + recipients[0].data = data; + } + const unsignedTxs = []; + for (let index = 0; index < recipients.length; index++) { + const rawTx = Transactions.create({ + ...txp, + ...recipients[index], + chain, + nonce: Number(txp.nonce) + Number(index), + recipients: [recipients[index]] + }); + unsignedTxs.push(rawTx); + } + + let tx = { + uncheckedSerialize: () => unsignedTxs, + txid: () => txp.txid, + toObject: () => { + let ret = _.clone(txp); + ret.outputs[0].satoshis = ret.outputs[0].amount; + return ret; + }, + getFee: () => { + return txp.fee; + }, + getChangeOutput: () => null + }; + + if (opts.signed) { + const sigs = txp.getCurrentSignatures(); + sigs.forEach(x => { + this.addSignaturesToBitcoreTx(tx, txp.inputs, txp.inputPaths, x.signatures, x.xpub); + }); + } + + return tx; + } + + convertFeePerKb(p, feePerKb) { + return [p, feePerKb]; + } + + checkTx(txp) { + try { + const tx = this.getBitcoreTx(txp); + } catch (ex) { + logger.debug('Error building Bitcore transaction', ex); + return ex; + } + + return null; + } + + checkTxUTXOs(server, txp, opts, cb) { + return cb(); + } + + selectTxInputs(server, txp, wallet, opts, cb) { + server.getBalance( + { wallet, tokenAddress: opts.tokenAddress, multisigContractAddress: opts.multisigContractAddress }, + (err, balance) => { + if (err) return cb(err); + + const getInvoiceValue = txp => { + let totalAmount; + + /* invoice outputs data example: + abiDecoder.decodeMethod(txp.outputs[0].data) + { name: 'approve', + params: + [ { name: '_spender', + value: '0xc27ed3df0de776246cdad5a052a9982473fceab8', + type: 'address' }, + { name: '_value', value: '1380623310000000', type: 'uint256' } ] } + + > abiDecoder.decodeMethod(txp.outputs[1].data) + { name: 'pay', + params: + [ { name: 'value', value: '1000000', type: 'uint256' }, + { name: 'gasPrice', value: '40000000000', type: 'uint256' }, + { name: 'expiration', value: '1604123733282', type: 'uint256' }, + ... ] } + */ + + txp.outputs.forEach(output => { + // We use a custom contract call (pay) instead of the transfer ERC20 method + const decodedData = getInvoiceDecoder().decodeMethod(output.data); + if (decodedData && decodedData.name === 'pay') { + totalAmount = decodedData.params[0].value; + } + }); + return totalAmount; + }; + + const { totalAmount, availableAmount } = balance; + + /* If its paypro its an already created ERC20 transaction and we need to get the actual invoice value from the data + invoice outputs example: + "outputs":[{ + "amount":0, + "toAddress":"0x44d69d16C711BF966E3d00A46f96e02D16BDdf1f", + "message":null, + "data":"...", + "gasLimit":29041 + }, + { + "amount":0, + "toAddress":"0xc27eD3DF0DE776246cdAD5a052A9982473FceaB8", + "message":null, + "data":"...", + "gasLimit":200000 + }] + */ + const txpTotalAmount = + (opts.multisigContractAddress || opts.tokenAddress) && txp.payProUrl + ? getInvoiceValue(txp) + : txp.getTotalAmount(opts); + + if (totalAmount < txpTotalAmount) { + return cb(Errors.INSUFFICIENT_FUNDS); + } else if (availableAmount < txpTotalAmount) { + return cb(Errors.LOCKED_FUNDS); + } else { + if (opts.tokenAddress || opts.multisigContractAddress) { + // RSK linked wallet balance + server.getBalance({}, (err, rskBalance) => { + if (err) return cb(err); + const { totalAmount, availableAmount } = rskBalance; + if (totalAmount < txp.fee) { + return cb( + new ClientError( + Errors.codes.INSUFFICIENT_ETH_FEE, + `${Errors.INSUFFICIENT_ETH_FEE.message}. RequiredFee: ${txp.fee}`, + { + requiredFee: txp.fee + } + ) + ); + } else if (availableAmount < txp.fee) { + return cb( + new ClientError( + Errors.codes.LOCKED_ETH_FEE, + `${Errors.LOCKED_ETH_FEE.message}. RequiredFee: ${txp.fee}`, + { + requiredFee: txp.fee + } + ) + ); + } else { + return cb(this.checkTx(txp)); + } + }); + } else if (availableAmount - txp.fee < txpTotalAmount) { + return cb( + new ClientError( + Errors.codes.INSUFFICIENT_FUNDS_FOR_FEE, + `${Errors.INSUFFICIENT_FUNDS_FOR_FEE.message}. RequiredFee: ${txp.fee}`, + { + requiredFee: txp.fee + } + ) + ); + } else { + return cb(this.checkTx(txp)); + } + } + } + ); + } + + checkUtxos(opts) {} + + checkValidTxAmount(output): boolean { + if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount < 0) { + return false; + } + return true; + } + + isUTXOCoin() { + return false; + } + isSingleAddress() { + return true; + } + + addressFromStorageTransform(network, address): void { + if (network != 'livenet') { + const x = address.address.indexOf(':' + network); + if (x >= 0) { + address.address = address.address.substr(0, x); + } + } + } + + addressToStorageTransform(network, address): void { + if (network != 'livenet') address.address += ':' + network; + } + + addSignaturesToBitcoreTx(tx, inputs, inputPaths, signatures, xpub) { + if (signatures.length === 0) { + throw new Error('Signatures Required'); + } + + const chain = 'RSK'; + const unsignedTxs = tx.uncheckedSerialize(); + const signedTxs = []; + for (let index = 0; index < signatures.length; index++) { + const signed = Transactions.applySignature({ + chain, + tx: unsignedTxs[index], + signature: signatures[index] + }); + signedTxs.push(signed); + + // bitcore users id for txid... + tx.id = Transactions.getHash({ tx: signed, chain }); + } + tx.uncheckedSerialize = () => signedTxs; + } + + validateAddress(wallet, inaddr, opts) { + const chain = 'RSK'; + const isValidTo = Validation.validateAddress(chain, wallet.network, inaddr); + if (!isValidTo) { + throw Errors.INVALID_ADDRESS; + } + const isValidFrom = Validation.validateAddress(chain, wallet.network, opts.from); + if (!isValidFrom) { + throw Errors.INVALID_ADDRESS; + } + return; + } + + onCoin(coin) { + return null; + } + + onTx(tx) { + // TODO: Multisig ERC20 - Internal txs ¿? + let tokenAddress; + let multisigContractAddress; + let address; + let amount; + if (tx.abiType && tx.abiType.type === 'ERC20') { + tokenAddress = tx.to; + address = Web3.utils.toChecksumAddress(tx.abiType.params[0].value); + amount = tx.abiType.params[1].value; + } else if (tx.abiType && tx.abiType.type === 'MULTISIG' && tx.abiType.name === 'submitTransaction') { + multisigContractAddress = tx.to; + address = Web3.utils.toChecksumAddress(tx.abiType.params[0].value); + amount = tx.abiType.params[1].value; + } else if (tx.abiType && tx.abiType.type === 'MULTISIG' && tx.abiType.name === 'confirmTransaction') { + multisigContractAddress = tx.to; + address = Web3.utils.toChecksumAddress(tx.internal[0].action.to); + amount = tx.internal[0].action.value; + } else { + address = tx.to; + amount = tx.value; + } + return { + txid: tx.txid, + out: { + address, + amount, + tokenAddress, + multisigContractAddress + } + }; + } +} diff --git a/packages/bitcore-wallet-service/src/lib/common/constants.ts b/packages/bitcore-wallet-service/src/lib/common/constants.ts index d77f8354b67..244928d7f5d 100644 --- a/packages/bitcore-wallet-service/src/lib/common/constants.ts +++ b/packages/bitcore-wallet-service/src/lib/common/constants.ts @@ -27,7 +27,9 @@ module.exports = { BUSD: 'busd', DAI: 'dai', WBTC: 'wbtc', - SHIB: 'shib' + SHIB: 'shib', + RSK: 'rsk', + RBTC: 'rbtc' }, ERC20: { diff --git a/packages/bitcore-wallet-service/src/lib/common/defaults.ts b/packages/bitcore-wallet-service/src/lib/common/defaults.ts index fde5bb59ff8..89817dfd4f6 100644 --- a/packages/bitcore-wallet-service/src/lib/common/defaults.ts +++ b/packages/bitcore-wallet-service/src/lib/common/defaults.ts @@ -83,6 +83,13 @@ module.exports = { defaultValue: 1000000000 } ], + rsk: [ + { + name: 'superEconomy', + nbBlocks: 1, + defaultValue: 59240000 + } + ], xrp: [ { name: 'normal', @@ -237,6 +244,7 @@ module.exports = { btc: 10000 * 1000, // 10k sat/b bch: 10000 * 1000, // 10k sat/b eth: 1000000000000, // 50 Gwei, + rsk: 1000000000000, xrp: 1000000000000, doge: 100000000 * 100, ltc: 10000 * 1000 // 10k sat/b @@ -248,7 +256,8 @@ module.exports = { eth: 0, xrp: 0, doge: 0, - ltc: 0 + ltc: 0, + rsk: 59240000 // minGasPrice }, MAX_TX_FEE: { @@ -257,7 +266,8 @@ module.exports = { eth: 1 * 1e18, // 1 eth xrp: 1 * 1e6, // 1 xrp doge: 400 * 1e8, - ltc: 0.05 * 1e8 + ltc: 0.05 * 1e8, + rsk: 1 * 1e18 // 1 rbtc }, // ETH diff --git a/packages/bitcore-wallet-service/src/lib/emailservice.ts b/packages/bitcore-wallet-service/src/lib/emailservice.ts index f4d875d9721..4fef548100e 100644 --- a/packages/bitcore-wallet-service/src/lib/emailservice.ts +++ b/packages/bitcore-wallet-service/src/lib/emailservice.ts @@ -245,7 +245,8 @@ export class EmailService { eth: 'ETH', xrp: 'XRP', doge: 'DOGE', - ltc: 'LTC' + ltc: 'LTC', + rsk: 'RSK' }; const data = _.cloneDeep(notification.data); diff --git a/packages/bitcore-wallet-service/src/lib/fiatrateservice.ts b/packages/bitcore-wallet-service/src/lib/fiatrateservice.ts index 8d003738baa..2210a720cc6 100644 --- a/packages/bitcore-wallet-service/src/lib/fiatrateservice.ts +++ b/packages/bitcore-wallet-service/src/lib/fiatrateservice.ts @@ -57,7 +57,7 @@ export class FiatRateService { _fetch(cb?) { cb = cb || function() {}; - const coins = ['btc', 'bch', 'eth', 'xrp', 'doge', 'ltc', 'shib']; + const coins = ['btc', 'bch', 'eth', 'xrp', 'doge', 'ltc', 'shib', 'rsk', 'rbtc']; const provider = this.providers[0]; // async.each(this.providers, (provider, next) => { @@ -251,7 +251,7 @@ export class FiatRateService { // Oldest date in timestamp range in epoch number ex. 24 hours ago const now = Date.now() - Defaults.FIAT_RATE_FETCH_INTERVAL * 60 * 1000; const ts = _.isNumber(opts.ts) ? opts.ts : now; - const coins = ['btc', 'bch', 'eth', 'xrp', 'doge', 'ltc', 'shib']; + const coins = ['btc', 'bch', 'eth', 'xrp', 'doge', 'ltc', 'shib', 'rsk', 'rbtc']; async.map( coins, diff --git a/packages/bitcore-wallet-service/src/lib/model/wallet.ts b/packages/bitcore-wallet-service/src/lib/model/wallet.ts index 583d6e01c0d..346c22a8db8 100644 --- a/packages/bitcore-wallet-service/src/lib/model/wallet.ts +++ b/packages/bitcore-wallet-service/src/lib/model/wallet.ts @@ -16,6 +16,7 @@ const Bitcore = { btc: require('bitcore-lib'), bch: require('bitcore-lib-cash'), eth: require('bitcore-lib'), + rsk: require('bitcore-lib'), xrp: require('bitcore-lib'), doge: require('bitcore-lib-doge'), ltc: require('bitcore-lib-ltc') diff --git a/packages/bitcore-wallet-service/src/lib/pushnotificationsservice.ts b/packages/bitcore-wallet-service/src/lib/pushnotificationsservice.ts index c1f8aeea6aa..18cbdad66a4 100644 --- a/packages/bitcore-wallet-service/src/lib/pushnotificationsservice.ts +++ b/packages/bitcore-wallet-service/src/lib/pushnotificationsservice.ts @@ -390,7 +390,9 @@ export class PushNotificationsService { busd: 'BUSD', wbtc: 'WBTC', dai: 'DAI', - shib: 'SHIB' + shib: 'SHIB', + rsk: 'RSK', + rbtc: 'RBTC' }; const data = _.cloneDeep(notification.data); data.subjectPrefix = _.trim(this.subjectPrefix + ' '); diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index 8573fb078f0..51086a9825c 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -42,6 +42,7 @@ const Bitcore_ = { bch: require('bitcore-lib-cash'), eth: Bitcore, xrp: Bitcore, + rsk: Bitcore, doge: require('bitcore-lib-doge'), ltc: require('bitcore-lib-ltc') }; @@ -499,6 +500,7 @@ export class WalletService { return cb(new ClientError('Invalid combination of required copayers / total copayers')); } + logger.info('checking coin: ' + opts.coin); opts.coin = opts.coin || Defaults.COIN; if (!Utils.checkValueInCollection(opts.coin, Constants.COINS)) { return cb(new ClientError('Invalid coin')); @@ -1172,7 +1174,7 @@ export class WalletService { this.getWallet({}, (err, wallet) => { if (err) return cb(err); - if (wallet.coin != 'eth') { + if (wallet.coin != 'eth' && wallet.coin != 'rsk') { opts.tokenAddresses = null; opts.multisigEthInfo = null; } @@ -2226,7 +2228,7 @@ export class WalletService { } getTokenContractInfo(opts) { - const bc = this._getBlockchainExplorer('eth', opts.network); + const bc = this._getBlockchainExplorer(opts.coin, opts.network); return new Promise((resolve, reject) => { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getTokenContractInfo(opts, (err, contractInfo) => { diff --git a/packages/bitcore-wallet-service/src/scripts/v8tool-list.ts b/packages/bitcore-wallet-service/src/scripts/v8tool-list.ts index 309d173597d..fc017282331 100755 --- a/packages/bitcore-wallet-service/src/scripts/v8tool-list.ts +++ b/packages/bitcore-wallet-service/src/scripts/v8tool-list.ts @@ -33,6 +33,7 @@ const BASE = { BTC: `https://api.bitcore.io/api/${coin}/${network}`, BCH: `https://api.bitcore.io/api/${coin}/${network}`, ETH: `https://api-eth.bitcore.io/api/${coin}/${network}`, + RSK: `https://api-rsk.bitcore.io/api/${coin}/${network}`, XRP: `https://api-xrp.bitcore.io/api/${coin}/${network}`, DOGE: `https://api.bitcore.io/api/${coin}/${network}`, LTC: `https://api.bitcore.io/api/${coin}/${network}` diff --git a/packages/bitcore-wallet-service/src/scripts/v8tool.ts b/packages/bitcore-wallet-service/src/scripts/v8tool.ts index 87ace599c13..b7dcac1ae82 100755 --- a/packages/bitcore-wallet-service/src/scripts/v8tool.ts +++ b/packages/bitcore-wallet-service/src/scripts/v8tool.ts @@ -32,6 +32,7 @@ const BASE = { BTC: `https://api.bitcore.io/api/${coin}/${network}`, BCH: `https://api.bitcore.io/api/${coin}/${network}`, ETH: `https://api-eth.bitcore.io/api/${coin}/${network}`, + RSK: `https://api-rsk.bitcore.io/api/${coin}/${network}`, XRP: `https://api-xrp.bitcore.io/api/${coin}/${network}`, DOGE: `https://api.bitcore.io/api/${coin}/${network}`, LTC: `https://api.bitcore.io/api/${coin}/${network}` diff --git a/packages/bitcore-wallet-service/test/integration/fiatrateservice.js b/packages/bitcore-wallet-service/test/integration/fiatrateservice.js index 18d6a688fbb..c6b38031d72 100644 --- a/packages/bitcore-wallet-service/test/integration/fiatrateservice.js +++ b/packages/bitcore-wallet-service/test/integration/fiatrateservice.js @@ -397,7 +397,21 @@ describe('Fiat rate service', function() { }, { code: 'EUR', rate: 0.00003276 - }] + }]; + var rsk = [{ + code: 'USD', + rate: 0.00003678 + }, { + code: 'EUR', + rate: 0.00003276 + }]; + var rbtc = [{ + code: 'USD', + rate: 0.00003678 + }, { + code: 'EUR', + rate: 0.00003276 + }]; request.get.withArgs({ url: 'https://bitpay.com/api/rates/BTC', @@ -427,6 +441,14 @@ describe('Fiat rate service', function() { url: 'https://bitpay.com/api/rates/SHIB', json: true }).yields(null, null, shib); + request.get.withArgs({ + url: 'https://bitpay.com/api/rates/RSK', + json: true + }).yields(null, null, rsk); + request.get.withArgs({ + url: 'https://bitpay.com/api/rates/RBTC', + json: true + }).yields(null, null, rbtc); service._fetch(function(err) { should.not.exist(err); @@ -484,8 +506,24 @@ describe('Fiat rate service', function() { should.not.exist(err); res.fetchedOn.should.equal(100); res.rate.should.equal(0.00003678); - clock.restore(); - done(); + service.getRate({ + code: 'USD', + coin: 'rsk' + }, function(err, res) { + should.not.exist(err); + res.fetchedOn.should.equal(100); + res.rate.should.equal(0.00003678); + service.getRate({ + code: 'USD', + coin: 'rbtc' + }, function(err, res) { + should.not.exist(err); + res.fetchedOn.should.equal(100); + res.rate.should.equal(0.00003678); + clock.restore(); + done(); + }) + }) }) }); }); diff --git a/packages/crypto-wallet-core/src/constants/units.ts b/packages/crypto-wallet-core/src/constants/units.ts index eac5553b7fc..2f145793811 100644 --- a/packages/crypto-wallet-core/src/constants/units.ts +++ b/packages/crypto-wallet-core/src/constants/units.ts @@ -32,6 +32,29 @@ export let UNITS = { minDecimals: 2 } }, + rsk: { + toSatoshis: 1e18, + full: { + maxDecimals: 8, + minDecimals: 8 + }, + short: { + maxDecimals: 6, + minDecimals: 2 + } + }, + rbtc: { + // Define if chain or coin + toSatoshis: 1e18, + full: { + maxDecimals: 8, + minDecimals: 8 + }, + short: { + maxDecimals: 6, + minDecimals: 2 + } + }, xrp: { toSatoshis: 1e6, full: { diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index bc7f1811f11..7a91c33462f 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -22,6 +22,7 @@ const derivers: { [chain: string]: IDeriver } = { BTC: new BtcDeriver(), BCH: new BchDeriver(), ETH: new EthDeriver(), + RSK: new EthDeriver(), XRP: new XrpDeriver(), DOGE: new DogeDeriver(), LTC: new LtcDeriver() diff --git a/packages/crypto-wallet-core/src/derivation/paths.ts b/packages/crypto-wallet-core/src/derivation/paths.ts index 36b31a448ba..f71304b6b2d 100644 --- a/packages/crypto-wallet-core/src/derivation/paths.ts +++ b/packages/crypto-wallet-core/src/derivation/paths.ts @@ -12,6 +12,11 @@ export const Paths = { livenet: "m/44'/60'/", testnet: "m/44'/60'/" }, + RSK: { + mainnet: "m/44'/137'/", + livenet: "m/44'/137'/", + testnet: "m/44'/37310'/" + }, XRP: { mainnet: "m/44'/144'/", livenet: "m/44'/144'/", diff --git a/packages/crypto-wallet-core/src/transactions/erc20/index.ts b/packages/crypto-wallet-core/src/transactions/erc20/index.ts index f4ccedbe301..d516efb90f5 100644 --- a/packages/crypto-wallet-core/src/transactions/erc20/index.ts +++ b/packages/crypto-wallet-core/src/transactions/erc20/index.ts @@ -5,6 +5,7 @@ import { ERC20Abi, MULTISENDAbi } from './abi'; const { toBN } = Web3.utils; export class ERC20TxProvider extends ETHTxProvider { + // TODO: extend erc20 to RSK (not only ETH) getERC20Contract(tokenContractAddress: string) { const web3 = new Web3(); const contract = new web3.eth.Contract(ERC20Abi as AbiItem[], tokenContractAddress); diff --git a/packages/crypto-wallet-core/src/transactions/eth-multisig/index.ts b/packages/crypto-wallet-core/src/transactions/eth-multisig/index.ts index 86df3054d56..20cc4a4ff19 100644 --- a/packages/crypto-wallet-core/src/transactions/eth-multisig/index.ts +++ b/packages/crypto-wallet-core/src/transactions/eth-multisig/index.ts @@ -4,6 +4,7 @@ import { ETHTxProvider } from '../eth'; import { MultisigAbi } from './abi'; export class ETHMULTISIGTxProvider extends ETHTxProvider { + // TODO: extend to RSK getMultisigContract(multisigContractAddress: string) { const web3 = new Web3(); const contract = new web3.eth.Contract(MultisigAbi as AbiItem[], multisigContractAddress); diff --git a/packages/crypto-wallet-core/src/transactions/index.ts b/packages/crypto-wallet-core/src/transactions/index.ts index 97d77623214..a5fa6bce2b8 100644 --- a/packages/crypto-wallet-core/src/transactions/index.ts +++ b/packages/crypto-wallet-core/src/transactions/index.ts @@ -5,6 +5,7 @@ import { ERC20TxProvider } from './erc20'; import { ETHTxProvider } from './eth'; import { ETHMULTISIGTxProvider } from './eth-multisig'; import { LTCTxProvider } from './ltc'; +import { RSKTxProvider } from './rsk'; import { XRPTxProvider } from './xrp'; const providers = { @@ -15,7 +16,8 @@ const providers = { ETHMULTISIG: new ETHMULTISIGTxProvider(), XRP: new XRPTxProvider(), DOGE: new DOGETxProvider(), - LTC: new LTCTxProvider() + LTC: new LTCTxProvider(), + RSK: new RSKTxProvider() // TODO: add multisig support }; export class TransactionsProxy { diff --git a/packages/crypto-wallet-core/src/transactions/rsk/index.ts b/packages/crypto-wallet-core/src/transactions/rsk/index.ts new file mode 100644 index 00000000000..1c3f65d56c3 --- /dev/null +++ b/packages/crypto-wallet-core/src/transactions/rsk/index.ts @@ -0,0 +1,122 @@ +import { ethers } from 'ethers'; +import Web3 from 'web3'; +import { AbiItem } from 'web3-utils'; +import { Key } from '../../derivation'; +import { ERC20Abi, MULTISENDAbi } from '../erc20/abi'; +const utils = require('web3-utils'); +const { toBN } = Web3.utils; +export class RSKTxProvider { + create(params: { + recipients: Array<{ address: string; amount: string }>; + nonce: number; + gasPrice: number; + data: string; + gasLimit: number; + network: string; + chainId?: number; + contractAddress?: string; + }) { + const { recipients, nonce, gasPrice, gasLimit, network, contractAddress } = params; + let { data } = params; + let to; + let amount; + if (recipients.length > 1) { + if (!contractAddress) { + throw new Error('Multiple recipients requires use of multi-send contract, please specify contractAddress'); + } + const addresses = []; + const amounts = []; + amount = toBN(0); + for (let recipient of recipients) { + addresses.push(recipient.address); + amounts.push(toBN(recipient.amount)); + amount = amount.add(toBN(recipient.amount)); + } + const multisendContract = this.getMultiSendContract(contractAddress); + data = data || multisendContract.methods.sendEth(addresses, amounts).encodeABI(); + to = contractAddress; + } else { + to = recipients[0].address; + amount = recipients[0].amount; + } + let { chainId } = params; + chainId = chainId || this.getChainId(network); + const txData = { + nonce: utils.toHex(nonce), + gasLimit: utils.toHex(gasLimit), + gasPrice: utils.toHex(gasPrice), + to, + data, + value: utils.toHex(amount), + chainId + }; + return ethers.utils.serializeTransaction(txData); + } + + getMultiSendContract(tokenContractAddress: string) { + const web3 = new Web3(); + return new web3.eth.Contract(MULTISENDAbi as AbiItem[], tokenContractAddress); + } + + getChainId(network: string) { + let chainId = 137; + switch (network) { + case 'testnet': + chainId = 37310; + break; + case 'regtest': + chainId = 37310; + break; + default: + chainId = 137; + break; + } + return chainId; + } + + getSignatureObject(params: { tx: string; key: Key }) { + const { tx, key } = params; + // To complain with new ethers + let k = key.privKey; + if (k.substr(0, 2) != '0x') { + k = '0x' + k; + } + + const signingKey = new ethers.utils.SigningKey(k); + const signDigest = signingKey.signDigest.bind(signingKey); + return signDigest(ethers.utils.keccak256(tx)); + } + + getSignature(params: { tx: string; key: Key }) { + const signatureHex = ethers.utils.joinSignature(this.getSignatureObject(params)); + return signatureHex; + } + + getHash(params: { tx: string }) { + const { tx } = params; + // tx must be signed, for hash to exist + return ethers.utils.parseTransaction(tx).hash; + } + + applySignature(params: { tx: string; signature: any }) { + let { tx, signature } = params; + const parsedTx = ethers.utils.parseTransaction(tx); + const { nonce, gasPrice, gasLimit, to, value, data, chainId } = parsedTx; + const txData = { nonce, gasPrice, gasLimit, to, value, data, chainId }; + if (typeof signature == 'string') { + signature = ethers.utils.splitSignature(signature); + } + const signedTx = ethers.utils.serializeTransaction(txData, signature); + const parsedTxSigned = ethers.utils.parseTransaction(signedTx); + if (!parsedTxSigned.hash) { + throw new Error('Signature invalid'); + } + return signedTx; + } + + sign(params: { tx: string; key: Key }) { + const { tx, key } = params; + const signature = this.getSignatureObject({ tx, key }); + return this.applySignature({ tx, signature }); + } +} diff --git a/packages/crypto-wallet-core/src/validation/index.ts b/packages/crypto-wallet-core/src/validation/index.ts index 7a95ae448c1..ca81169c477 100644 --- a/packages/crypto-wallet-core/src/validation/index.ts +++ b/packages/crypto-wallet-core/src/validation/index.ts @@ -3,6 +3,7 @@ import { BtcValidation } from './btc'; import { DogeValidation } from './doge'; import { EthValidation } from './eth'; import { LtcValidation } from './ltc'; +import { RskValidation } from './rsk'; import { XrpValidation } from './xrp'; export interface IValidation { @@ -16,7 +17,8 @@ const validation: { [chain: string]: IValidation } = { ETH: new EthValidation(), XRP: new XrpValidation(), DOGE: new DogeValidation(), - LTC: new LtcValidation() + LTC: new LtcValidation(), + RSK: new RskValidation() }; export class ValidationProxy { diff --git a/packages/crypto-wallet-core/src/validation/rsk/index.ts b/packages/crypto-wallet-core/src/validation/rsk/index.ts new file mode 100644 index 00000000000..89d08bbf0a5 --- /dev/null +++ b/packages/crypto-wallet-core/src/validation/rsk/index.ts @@ -0,0 +1,23 @@ +import { IValidation } from '..'; +const utils = require('web3-utils'); + +export class RskValidation implements IValidation { + validateAddress(_network: string, address: string): boolean { + return utils.isAddress(address); // TODO: add RSK checksum + } + + validateUri(addressUri: string): boolean { + if (!addressUri) { + return false; + } + const address = this.extractAddress(addressUri); + const rskPrefix = /rsk/i.exec(addressUri); + return !!rskPrefix && utils.isAddress(address); + } + + private extractAddress(data) { + const prefix = /^[a-z]+:/i; + const params = /([\?\&](value|gas|gasPrice|gasLimit)=(\d+([\,\.]\d+)?))+/i; + return data.replace(prefix, '').replace(params, ''); + } +} diff --git a/packages/insight/src/assets/img/currency_logos/rsk.svg b/packages/insight/src/assets/img/currency_logos/rsk.svg new file mode 100644 index 00000000000..6cc8dd5a1e0 --- /dev/null +++ b/packages/insight/src/assets/img/currency_logos/rsk.svg @@ -0,0 +1,15 @@ + + + + config/icon-reserve + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/packages/insight/src/assets/img/rsk-testnet.svg b/packages/insight/src/assets/img/rsk-testnet.svg new file mode 100644 index 00000000000..6cc8dd5a1e0 --- /dev/null +++ b/packages/insight/src/assets/img/rsk-testnet.svg @@ -0,0 +1,15 @@ + + + + config/icon-reserve + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/packages/insight/src/components/denomination/denomination.html b/packages/insight/src/components/denomination/denomination.html index 969fe1564df..c9375934d08 100644 --- a/packages/insight/src/components/denomination/denomination.html +++ b/packages/insight/src/components/denomination/denomination.html @@ -7,6 +7,7 @@ + diff --git a/packages/insight/src/components/latest-blocks/latest-blocks.ts b/packages/insight/src/components/latest-blocks/latest-blocks.ts index b532bd661ba..02825cf3e9b 100644 --- a/packages/insight/src/components/latest-blocks/latest-blocks.ts +++ b/packages/insight/src/components/latest-blocks/latest-blocks.ts @@ -66,7 +66,7 @@ export class LatestBlocksComponent implements OnInit, OnDestroy { if (UTXO_CHAINS.includes(this.chainNetwork.chain)) { return this.blocksProvider.toUtxoCoinAppBlock(block); } - if (this.chainNetwork.chain === 'ETH') { + if (this.chainNetwork.chain === 'ETH' || this.chainNetwork.chain === 'RSK') { return this.blocksProvider.toEthAppBlock(block); } } @@ -105,7 +105,7 @@ export class LatestBlocksComponent implements OnInit, OnDestroy { ) { return this.blocksProvider.toUtxoCoinAppBlock(block); } - if (this.chainNetwork.chain === 'ETH') { + if (this.chainNetwork.chain === 'ETH' || this.chainNetwork.chain === 'RSK') { return this.blocksProvider.toEthAppBlock(block); } } diff --git a/packages/insight/src/components/transaction-details/transaction-details.ts b/packages/insight/src/components/transaction-details/transaction-details.ts index 30de66d97bc..fc93339c240 100644 --- a/packages/insight/src/components/transaction-details/transaction-details.ts +++ b/packages/insight/src/components/transaction-details/transaction-details.ts @@ -47,7 +47,7 @@ export class TransactionDetailsComponent implements OnInit { public ngOnInit(): void { this.getConfirmations(); - if (this.chainNetwork.chain !== 'ETH') { + if (this.chainNetwork.chain !== 'ETH' && this.chainNetwork.chain !== 'RSK') { if (!this.tx.vin || !this.tx.vin.length) { this.getCoins(); } diff --git a/packages/insight/src/components/transaction-list/transaction-list.html b/packages/insight/src/components/transaction-list/transaction-list.html index 1c0ddd87995..586ee027fc1 100644 --- a/packages/insight/src/components/transaction-list/transaction-list.html +++ b/packages/insight/src/components/transaction-list/transaction-list.html @@ -5,7 +5,7 @@ - + diff --git a/packages/insight/src/pages/block-detail/block-detail.html b/packages/insight/src/pages/block-detail/block-detail.html index 35ca9f4e704..512345dea9e 100644 --- a/packages/insight/src/pages/block-detail/block-detail.html +++ b/packages/insight/src/pages/block-detail/block-detail.html @@ -24,7 +24,7 @@

Summary

- + Number of Transactions diff --git a/packages/insight/src/pages/block-detail/block-detail.ts b/packages/insight/src/pages/block-detail/block-detail.ts index 44645362630..91cd1391439 100644 --- a/packages/insight/src/pages/block-detail/block-detail.ts +++ b/packages/insight/src/pages/block-detail/block-detail.ts @@ -57,7 +57,7 @@ export class BlockDetailPage { if (UTXO_CHAINS.includes(this.chainNetwork.chain)) { block = this.blocksProvider.toUtxoCoinAppBlock(response); } - if (this.chainNetwork.chain === 'ETH') { + if (this.chainNetwork.chain === 'ETH' || this.chainNetwork.chain === 'RSK') { block = this.blocksProvider.toEthAppBlock(response); } this.block = block; diff --git a/packages/insight/src/pages/home/home.html b/packages/insight/src/pages/home/home.html index 163f64e7835..8ffcaf4b3dd 100644 --- a/packages/insight/src/pages/home/home.html +++ b/packages/insight/src/pages/home/home.html @@ -13,6 +13,7 @@

Latest Blocks

+ diff --git a/packages/insight/src/pages/search/search.html b/packages/insight/src/pages/search/search.html index 0abe0e6769c..07afe1ab415 100644 --- a/packages/insight/src/pages/search/search.html +++ b/packages/insight/src/pages/search/search.html @@ -13,6 +13,7 @@

Blocks:

+ @@ -40,6 +41,7 @@

Transactions:

+ @@ -65,6 +67,7 @@

Addresses:

+ diff --git a/packages/insight/src/pages/transaction/transaction.html b/packages/insight/src/pages/transaction/transaction.html index d83453745b4..fae92a19ce6 100644 --- a/packages/insight/src/pages/transaction/transaction.html +++ b/packages/insight/src/pages/transaction/transaction.html @@ -30,7 +30,7 @@

Summary

- + Received Time @@ -55,7 +55,7 @@

Summary

Details

- + diff --git a/packages/insight/src/pages/transaction/transaction.ts b/packages/insight/src/pages/transaction/transaction.ts index ae90f17a78c..0b0440340c6 100644 --- a/packages/insight/src/pages/transaction/transaction.ts +++ b/packages/insight/src/pages/transaction/transaction.ts @@ -58,7 +58,7 @@ export class TransactionPage { if (UTXO_CHAINS.includes(this.chainNetwork.chain)) { tx = this.txProvider.toUtxoCoinsAppTx(response); } - if (this.chainNetwork.chain === 'ETH') { + if (this.chainNetwork.chain === 'ETH' || this.chainNetwork.chain === 'RSK') { tx = this.txProvider.toEthAppTx(response); } this.tx = tx; diff --git a/packages/insight/src/providers/api/api.ts b/packages/insight/src/providers/api/api.ts index 3387418813a..ee67a1449dd 100644 --- a/packages/insight/src/providers/api/api.ts +++ b/packages/insight/src/providers/api/api.ts @@ -42,7 +42,8 @@ export class ApiProvider { btc: 'https://bitpay.com/api/rates', bch: 'https://bitpay.com/api/rates/bch', doge: 'https://bitpay.com/api/rates/doge', - eth: 'https://bitpay.com/api/rates/eth' + eth: 'https://bitpay.com/api/rates/eth', + rsk: 'https://bitpay.com/api/rates/rsk' }; public bwsUrl = { diff --git a/packages/insight/src/providers/currency/currency.ts b/packages/insight/src/providers/currency/currency.ts index 33591d7748d..457722e2d37 100644 --- a/packages/insight/src/providers/currency/currency.ts +++ b/packages/insight/src/providers/currency/currency.ts @@ -39,6 +39,7 @@ export class CurrencyProvider { // TODO: Change this function to make use of satoshis so that we don't have to do all these roundabout conversions. switch (chain) { case 'ETH': + case 'RSK': value = value * 1e-18; break; default: diff --git a/packages/insight/src/providers/price/price.ts b/packages/insight/src/providers/price/price.ts index a0310622cd7..e9dfbf0c65b 100644 --- a/packages/insight/src/providers/price/price.ts +++ b/packages/insight/src/providers/price/price.ts @@ -24,6 +24,7 @@ export class PriceProvider { let ratesAPI; switch (this.api.getConfig().chain) { case 'BTC': + case 'RSK': // 1 RBTC = 1 BTC ratesAPI = this.api.ratesAPI.btc; break; case 'BCH': diff --git a/packages/insight/src/providers/search/search.ts b/packages/insight/src/providers/search/search.ts index d2627495bc5..8237f381fe0 100644 --- a/packages/insight/src/providers/search/search.ts +++ b/packages/insight/src/providers/search/search.ts @@ -90,6 +90,15 @@ export class SearchProvider { { chain: 'ETH', network: 'testnet' } ], }, + // RSK Address + { + regexes: [/^0x[a-fA-F0-9]{40}$/], + type: 'address', + chainNetworks: [ + { chain: 'RSK', network: 'mainnet' }, + { chain: 'RSK', network: 'testnet' } + ], + }, // Doge Address { regexes: [/^(dogecoin:)?(D[5-9A-HJ-NP-U][1-9A-HJ-NP-Za-km-z]{32})/], @@ -132,6 +141,15 @@ export class SearchProvider { { chain: 'ETH', network: 'testnet' } ], }, + // RSK block or tx + { + regexes: [/^0x[A-Fa-f0-9]{64}$/], + type: 'blockOrTx', + chainNetworks: [ + { chain: 'RSK', network: 'mainnet' }, + { chain: 'RSK', network: 'testnet' } + ], + }, // BTC / BCH / DOGE / ETH / LTC block height { regexes: [/^[0-9]{1,9}$/], @@ -141,11 +159,13 @@ export class SearchProvider { { chain: 'BCH', network: 'mainnet' }, { chain: 'DOGE', network: 'mainnet' }, { chain: 'ETH', network: 'mainnet' }, + { chain: 'RSK', network: 'mainnet' }, { chain: 'LTC', network: 'mainnet' }, { chain: 'BTC', network: 'testnet' }, { chain: 'BCH', network: 'testnet' }, { chain: 'DOGE', network: 'testnet' }, { chain: 'ETH', network: 'testnet' }, + { chain: 'RSK', network: 'testnet' }, { chain: 'LTC', network: 'testnet' } ], },