From 69cc153c01a41de00afbf155a2a4cb47b09b8ec2 Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 17 Aug 2020 17:28:14 +0200 Subject: [PATCH 1/8] Rename files & initial ts setup --- index.js => index.ts | 0 jsconfig.json | 8 -------- package.json | 5 +++-- src/{BitcoinHelpers.js => BitcoinHelpers.ts} | 10 ++++++---- src/{Constants.js => Constants.ts} | 0 src/{Deposit.js => Deposit.ts} | 0 src/{EthereumHelpers.js => EthereumHelpers.ts} | 0 src/{Redemption.js => Redemption.ts} | 0 src/{TBTC.js => TBTC.ts} | 0 src/lib.d.ts | 11 +++++++++++ tsconfig.json | 15 +++++++++++++++ 11 files changed, 35 insertions(+), 14 deletions(-) rename index.js => index.ts (100%) delete mode 100644 jsconfig.json rename src/{BitcoinHelpers.js => BitcoinHelpers.ts} (98%) rename src/{Constants.js => Constants.ts} (100%) rename src/{Deposit.js => Deposit.ts} (100%) rename src/{EthereumHelpers.js => EthereumHelpers.ts} (100%) rename src/{Redemption.js => Redemption.ts} (100%) rename src/{TBTC.js => TBTC.ts} (100%) create mode 100644 src/lib.d.ts create mode 100644 tsconfig.json diff --git a/index.js b/index.ts similarity index 100% rename from index.js rename to index.ts diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 730e5aa..0000000 --- a/jsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "module": "es6", - "target": "es6" - }, - "checkJs": true, - "exclude": ["node_modules"] -} diff --git a/package.json b/package.json index 10ead2c..8685045 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,9 @@ "chai": "^4.2.0", "eslint": "^6.8.0", "eslint-config-keep": "git+https://github.com/keep-network/eslint-config-keep.git#0.3.0", - "prettier": "^1.19.1", "fs": "0.0.1-security", - "mocha": "^6.2.0" + "mocha": "^6.2.0", + "prettier": "^1.19.1", + "typescript": "^3.9.7" } } diff --git a/src/BitcoinHelpers.js b/src/BitcoinHelpers.ts similarity index 98% rename from src/BitcoinHelpers.js rename to src/BitcoinHelpers.ts index f110e6a..c2fe6e0 100644 --- a/src/BitcoinHelpers.js +++ b/src/BitcoinHelpers.ts @@ -6,9 +6,11 @@ import BcoinScript from "bcoin/lib/script/index.js" import { BitcoinSPV } from "./lib/BitcoinSPV.js" /** @typedef { import("./lib/BitcoinSPV.js").Proof } Proof */ +import {Proof} from './lib/BitcoinSPV' import { BitcoinTxParser } from "./lib/BitcoinTxParser.js" import ElectrumClient from "./lib/ElectrumClient.js" /** @typedef { import("./lib/ElectrumClient.js").Config } ElectrumConfig */ +import {Config as ElectrumConfig} from "./lib/ElectrumClient" import BN from "bn.js" @@ -57,7 +59,7 @@ const BitcoinHelpers = { Network: BitcoinNetwork, /** @type {ElectrumConfig?} */ - electrumConfig: null, + electrumConfig: null as ElectrumConfig|null, /** * Updates the config to use for Electrum client connections. Electrum is @@ -66,7 +68,7 @@ const BitcoinHelpers = { * @param {ElectrumConfig} newConfig The config to use for future Electrum * connections. */ - setElectrumConfig: function(newConfig) { + setElectrumConfig: function(newConfig:ElectrumConfig) { BitcoinHelpers.electrumConfig = newConfig }, @@ -82,7 +84,7 @@ const BitcoinHelpers = { * * @return {Buffer} The signature in the DER format. */ - signatureDER: function(r, s) { + signatureDER: function(r:string, s:string) { const size = secp256k1.size const signature = new BcryptoSignature( size, @@ -211,7 +213,7 @@ const BitcoinHelpers = { * completes (successfully or unsuccessfully). * @template T */ - withElectrumClient: async function(block) { + withElectrumClient: async function(block:(client:ElectrumClient)=>Promise) { const electrumClient = new ElectrumClient(BitcoinHelpers.electrumConfig) await electrumClient.connect() diff --git a/src/Constants.js b/src/Constants.ts similarity index 100% rename from src/Constants.js rename to src/Constants.ts diff --git a/src/Deposit.js b/src/Deposit.ts similarity index 100% rename from src/Deposit.js rename to src/Deposit.ts diff --git a/src/EthereumHelpers.js b/src/EthereumHelpers.ts similarity index 100% rename from src/EthereumHelpers.js rename to src/EthereumHelpers.ts diff --git a/src/Redemption.js b/src/Redemption.ts similarity index 100% rename from src/Redemption.js rename to src/Redemption.ts diff --git a/src/TBTC.js b/src/TBTC.ts similarity index 100% rename from src/TBTC.js rename to src/TBTC.ts diff --git a/src/lib.d.ts b/src/lib.d.ts new file mode 100644 index 0000000..2fc698f --- /dev/null +++ b/src/lib.d.ts @@ -0,0 +1,11 @@ +declare module "bcoin/lib/bcoin-browser.js" { + export class Script{ + static fromAddress(address:string):Script; + } +} + +declare module "bcoin/lib/bcoin-browser.js" {} +declare module "bcrypto/lib/secp256k1.js" {} +declare module "bcrypto/lib/internal/signature.js" {} +declare module "bcoin/lib/primitives/index.js" {} +declare module "bcoin/lib/script/index.js" {} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..87f41be --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "strict": true, + "esModuleInterop": true, + "allowJs":true, + "skipLibCheck": false, + "outDir": "build", + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "declaration": true, + "noUnusedParameters": true + }, + "exclude": ["node_modules", "build"] +} From 0ca3027efe3d8597b23d078986fd69c7ccb79d7a Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 17 Aug 2020 17:28:36 +0200 Subject: [PATCH 2/8] Typefy BitcoinHelpers --- src/BitcoinHelpers.ts | 132 ++++++++++++++++++++++++------------------ src/lib.d.ts | 82 ++++++++++++++++++++++++-- 2 files changed, 154 insertions(+), 60 deletions(-) diff --git a/src/BitcoinHelpers.ts b/src/BitcoinHelpers.ts index c2fe6e0..aac9821 100644 --- a/src/BitcoinHelpers.ts +++ b/src/BitcoinHelpers.ts @@ -1,8 +1,8 @@ import bcoin from "bcoin/lib/bcoin-browser.js" import secp256k1 from "bcrypto/lib/secp256k1.js" import BcryptoSignature from "bcrypto/lib/internal/signature.js" -import BcoinPrimitives from "bcoin/lib/primitives/index.js" -import BcoinScript from "bcoin/lib/script/index.js" +import {KeyRing} from "bcoin/lib/primitives/index.js" +import {Script} from "bcoin/lib/script/index.js" import { BitcoinSPV } from "./lib/BitcoinSPV.js" /** @typedef { import("./lib/BitcoinSPV.js").Proof } Proof */ @@ -14,16 +14,15 @@ import {Config as ElectrumConfig} from "./lib/ElectrumClient" import BN from "bn.js" -const { KeyRing } = BcoinPrimitives -const { Script } = BcoinScript - /** @enum {string} */ const BitcoinNetwork = { TESTNET: "testnet", - MAINNET: "mainnet", + MAINNET: "main", SIMNET: "simnet" } +type BitcoinNetworkType = "testnet" |"main" | "simnet" + /** * Found transaction details. * @typedef FoundTransaction @@ -32,6 +31,11 @@ const BitcoinNetwork = { * @property {number} outputPosition Position of output in the transaction. * @property {number} value Value of the output (satoshis). */ +interface FoundTransaction{ + transactionID:string, + outputPosition:number, + value:number, +} /** * @typedef {Object} ParsedTransaction @@ -45,6 +49,12 @@ const BitcoinNetwork = { * @property {string} locktime The transaction locktime as an unprefixed hex * string. */ +interface ParsedTransaction{ + version:string, + txInVector:string, + txOutVector:string, + locktime:string, +} /** * @typedef {Object} SPVProof @@ -53,6 +63,10 @@ const BitcoinNetwork = { * additional data useful in submitting SPV proofs, stored as buffers. */ +interface SPVProof extends Proof { + parsedTransaction:ParsedTransaction; +} + const BitcoinHelpers = { satoshisPerBtc: new BN(10).pow(new BN(8)), @@ -116,18 +130,18 @@ const BitcoinHelpers = { * @return {string} An unprefixed, concatenated hex representation of the two * given coordinates. */ - publicKeyPointToPublicKeyString: function(publicKeyX, publicKeyY) { + publicKeyPointToPublicKeyString: function(publicKeyX:HexString, publicKeyY:HexString):HexString { return `${publicKeyX.replace("0x", "")}${publicKeyY.replace("0x", "")}` }, Address: { - pubKeyHashFrom: function(address) { - const script = bcoin.Script.fromAddress(address) + pubKeyHashFrom: function(address:string) { + const script = Script.fromAddress(address) return script.getWitnessPubkeyhash() }, publicKeyPointToP2WPKHAddress: function( - publicKeyX, - publicKeyY, - bitcoinNetwork + publicKeyX:HexString, + publicKeyY:HexString, + bitcoinNetwork:BitcoinNetworkType ) { return this.publicKeyToP2WPKHAddress( BitcoinHelpers.publicKeyPointToPublicKeyString(publicKeyX, publicKeyY), @@ -138,12 +152,12 @@ const BitcoinHelpers = { * Converts the specified `pubKeyHash` to a valid Bech32 address for * the specified `network`. * - * @param {string} pubKeyHash A pubKeyHash as a string. + * @param {string} pubKeyHash A pubKeyHash as a hexadecimal string. * @param {string} network The Bitcoin network for the Bech32 address. * * @return {string} A Bech32 address to */ - pubKeyHashToBech32: function(pubKeyHash, network) { + pubKeyHashToBech32: function(pubKeyHash:HexString, network:BitcoinNetworkType):string { return Script.fromProgram(0, Buffer.from(pubKeyHash, "hex")) .getAddress() .toBech32(network) @@ -156,7 +170,7 @@ const BitcoinHelpers = { * @param {BitcoinNetwork} network Network for which address has to be calculated. * @return {string} A Bitcoin P2WPKH address for given network. */ - publicKeyToP2WPKHAddress: function(publicKeyString, network) { + publicKeyToP2WPKHAddress: function(publicKeyString:string, network:BitcoinNetworkType) { const publicKeyBytes = Buffer.from(publicKeyString, "hex") // Witness program requires usage of compressed public keys. @@ -178,7 +192,7 @@ const BitcoinHelpers = { * @return {string} A Bitcoin script for the given address, as an * unprefixed hex string. */ - toScript: function(address) { + toScript: function(address:string):HexString { return BitcoinHelpers.Address.toRawScript(address).toString("hex") }, /** @@ -187,10 +201,10 @@ const BitcoinHelpers = { * * @param {string} address A Bitcoin address. * - * @return {string} A Bitcoin script for the given address, as a Buffer + * @return {Buffer} A Bitcoin script for the given address, as a Buffer * of bytes. */ - toRawScript: function(address) { + toRawScript: function(address:string):Buffer { return Script.fromAddress(address).toRaw() } }, @@ -214,6 +228,9 @@ const BitcoinHelpers = { * @template T */ withElectrumClient: async function(block:(client:ElectrumClient)=>Promise) { + if(BitcoinHelpers.electrumConfig === null){ + throw new Error("No electrum config has been provided") + } const electrumClient = new ElectrumClient(BitcoinHelpers.electrumConfig) await electrumClient.connect() @@ -245,7 +262,7 @@ const BitcoinHelpers = { * either null if such a transaction could not be found, or the * information about the transaction that was found. */ - find: async function(bitcoinAddress, expectedValue) { + find: async function(bitcoinAddress:string, expectedValue:number):Promise { const script = BitcoinHelpers.Address.toScript(bitcoinAddress) return await BitcoinHelpers.Transaction.findScript(script, expectedValue) @@ -265,13 +282,17 @@ const BitcoinHelpers = { * either null if such a transaction could not be found, or the * information about the transaction that was found. */ - findScript: async function(outputScript, expectedValue) { - return await BitcoinHelpers.withElectrumClient(electrumClient => { - return BitcoinHelpers.Transaction.findWithClient( + findScript: async function(outputScript:string, expectedValue:number):Promise { + return await BitcoinHelpers.withElectrumClient(async electrumClient => { + const foundTx = await BitcoinHelpers.Transaction.findWithClient( electrumClient, outputScript, expectedValue ) + if(foundTx === undefined){ + throw new Error("No transaction could be found") + } + return foundTx }) }, /** @@ -284,13 +305,13 @@ const BitcoinHelpers = { * @return {Promise} A promise to the found * transaction once it is seen on the chain. */ - findOrWaitFor: async function(bitcoinAddress, expectedValue) { + findOrWaitFor: async function(bitcoinAddress:string, expectedValue:number):Promise { return await BitcoinHelpers.withElectrumClient(async electrumClient => { const script = BitcoinHelpers.Address.toScript(bitcoinAddress) // This function is used as a callback to electrum client. It is // invoked when an existing or a new transaction is found. - const checkTransactions = async function(status) { + const checkTransactions = async function(status:boolean) { // If the status is set, transactions were seen for the // script. if (status) { @@ -322,9 +343,9 @@ const BitcoinHelpers = { * at least `requiredConfirmations` confirmations. */ checkForConfirmations: async function( - transactionID, - requiredConfirmations - ) { + transactionID:HexString, + requiredConfirmations:number + ):Promise { return BitcoinHelpers.withElectrumClient(async electrumClient => { const { confirmations } = await electrumClient.getTransaction( transactionID @@ -348,10 +369,10 @@ const BitcoinHelpers = { * observed that was at least equal to the required confirmations. */ waitForConfirmations: async function( - transactionID, - requiredConfirmations, - onReceivedConfirmation - ) { + transactionID:HexString, + requiredConfirmations:number, + onReceivedConfirmation:(tx:{ transactionID:HexString, confirmations:number })=>void + ):Promise { return BitcoinHelpers.withElectrumClient(async electrumClient => { const checkConfirmations = async function() { return BitcoinHelpers.withElectrumClient(async electrumClient => { @@ -385,7 +406,7 @@ const BitcoinHelpers = { * @return {Promise} The estimated fee to execute the provided * transaction. */ - estimateFee: async function(tbtcConstantsContract) { + estimateFee: async function(tbtcConstantsContract:any):Promise { return tbtcConstantsContract.methods.getMinimumRedemptionFee().call() }, /** @@ -398,9 +419,9 @@ const BitcoinHelpers = { * @param {number} confirmations The number of confirmations to include * in the proof. * - * @return {SPVProof} The proof data, plus the parsed transaction for the proof. + * @return {Promise} The proof data, plus the parsed transaction for the proof. */ - getSPVProof: async function(transactionID, confirmations) { + getSPVProof: async function(transactionID:HexString, confirmations:number):Promise { return await BitcoinHelpers.withElectrumClient(async electrumClient => { const spv = new BitcoinSPV(electrumClient) const proof = await spv.getTransactionProof( @@ -423,7 +444,7 @@ const BitcoinHelpers = { * @return {Promise} A partial FoundTransaction with * the transactionID field set. */ - broadcast: async function(signedTransaction) { + broadcast: async function(signedTransaction:HexString) { return await BitcoinHelpers.withElectrumClient(async electrumClient => { const transactionID = await electrumClient.broadcastTransaction( signedTransaction @@ -449,12 +470,12 @@ const BitcoinHelpers = { * signature. */ addWitnessSignature: function( - unsignedTransaction, - inputIndex, - r, - s, - publicKey - ) { + unsignedTransaction:HexString, + inputIndex:number, + r:HexString, + s:HexString, + publicKey:HexString + ):HexString { // Signature let signatureDER try { @@ -463,7 +484,7 @@ const BitcoinHelpers = { throw new Error(`failed to convert signature to DER format: [${err}]`) } - const hashType = Buffer.from([bcoin.Script.hashType.ALL]) + const hashType = Buffer.from([Script.hashType.ALL]) const sig = Buffer.concat([signatureDER, hashType]) // Public Key @@ -509,11 +530,11 @@ const BitcoinHelpers = { * @return {string} Raw bitcoin transaction in hexadecimal format. */ constructOneInputOneOutputWitnessTransaction( - previousOutpoint, - inputSequence, - outputValue, - outputScript - ) { + previousOutpoint:HexString, + inputSequence:number, + outputValue:number, + outputScript:HexString + ):HexString { // Input const prevOutpoint = bcoin.Outpoint.fromRaw( Buffer.from(previousOutpoint, "hex") @@ -548,7 +569,7 @@ const BitcoinHelpers = { * * @return {Promise} A promise to the found array of transactions. */ - findAllUnspent: async function(bitcoinAddress) { + findAllUnspent: async function(bitcoinAddress:string):Promise { return await BitcoinHelpers.withElectrumClient(async electrumClient => { const script = BitcoinHelpers.Address.toScript(bitcoinAddress) return BitcoinHelpers.Transaction.findAllUnspentWithClient( @@ -564,10 +585,10 @@ const BitcoinHelpers = { * * @return {Promise} A promise to the confirmed balance in satoshis. */ - getBalance: async function(bitcoinAddress) { + getBalance: async function(bitcoinAddress:string):Promise { return await BitcoinHelpers.withElectrumClient(async electrumClient => { const script = BitcoinHelpers.Address.toScript(bitcoinAddress) - return (await electrumClient.getBalanceOfScript(script)).confirmed + return ((await electrumClient.getBalanceOfScript(script)) as any).confirmed }) }, @@ -588,10 +609,10 @@ const BitcoinHelpers = { * information about the transaction that was found. */ findWithClient: async function( - electrumClient, - receiverScript, - expectedValue - ) { + electrumClient:ElectrumClient, + receiverScript:string, + expectedValue:number + ):Promise { const unspentTransactions = await electrumClient.getUnspentToScript( receiverScript ) @@ -605,6 +626,7 @@ const BitcoinHelpers = { } } } + return undefined }, /** * Finds all transactions to the given `receiverScript` using the @@ -617,7 +639,7 @@ const BitcoinHelpers = { * transactionID, outputPosition, and value. Resolves with * empty if no transactions exist. */ - findAllUnspentWithClient: async function(electrumClient, receiverScript) { + findAllUnspentWithClient: async function(electrumClient:ElectrumClient, receiverScript:string):Promise { const unspentTransactions = await electrumClient.getUnspentToScript( receiverScript ) diff --git a/src/lib.d.ts b/src/lib.d.ts index 2fc698f..1e91406 100644 --- a/src/lib.d.ts +++ b/src/lib.d.ts @@ -1,11 +1,83 @@ declare module "bcoin/lib/bcoin-browser.js" { + export class Outpoint{ + static fromRaw(data:Buffer):Outpoint + } + class Witness { + fromItems(items:Buffer[]):Witness + } + export class Input{ + static fromOptions(options:{ + prevout: Outpoint, + sequence: number, + script?:any, + witness?:any + }):Input + witness:Witness + } + export class TX{ + static fromOptions(options:{ + version?:number, + locktime?:number, + inputs:Input[], + outputs:Output[] + }):TX + toRaw():Buffer + static fromRaw(data:Buffer|string, enc?:string):TX + clone():TX + inputs:Input[] + } + export class Output{ + static fromOptions(options:{ + value: number + } & ({ + script: Buffer + }|{ + address:string + })):Output + } +} +declare module "bcrypto/lib/secp256k1.js" { + class ECDSA{ + get size():number; + signatureNormalize(sig:any):any; + publicKeyImport(json:Object, compress:any):any + } + const ECDSAimpl:ECDSA + export default ECDSAimpl; +} +declare module "bcrypto/lib/internal/signature.js" {} +declare module "bcoin/lib/primitives/index.js" { + export class KeyRing{ + static fromKey(key:Buffer, compress?:boolean):KeyRing; + // getKeyHash is a little hard to type right, as the return type depends on the input parameter + // but given that it's only called without arguments in the case we will only add types for that + // See https://github.com/bcoin-org/bcoin/blob/master/lib/primitives/keyring.js#L566 + getKeyHash():Buffer; + } +} +declare module "bcoin/lib/script/index.js" { + type network = `main`|`testnet`|`regtest`|`segnet4`|'simnet' + class Address{ + toBech32(network:network):string + toString(network:network):string + } + export class Script{ static fromAddress(address:string):Script; + static fromProgram(version:number, data:Buffer):Script; + static hashType:{ ALL: 1, NONE: 2, SINGLE: 3, ANYONECANPAY: 128 }; + getWitnessPubkeyhash():Buffer|null; + toRaw():Buffer; + getAddress():Address } } -declare module "bcoin/lib/bcoin-browser.js" {} -declare module "bcrypto/lib/secp256k1.js" {} -declare module "bcrypto/lib/internal/signature.js" {} -declare module "bcoin/lib/primitives/index.js" {} -declare module "bcoin/lib/script/index.js" {} \ No newline at end of file +declare module "bcrypto/lib/internal/signature.js" { + export default class { + constructor(size:number, r:Buffer, s:Buffer) + static toDER(raw:Buffer, size:number):Buffer + encode(size:number):Buffer + } +} + +type HexString=string \ No newline at end of file From 28a085bea67ff8ce5545a1c72f1d1b34c53c926b Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 17 Aug 2020 17:29:18 +0200 Subject: [PATCH 3/8] Remove unused code (Address.js) --- src/lib/Address.js | 75 ---------------------------------------------- 1 file changed, 75 deletions(-) delete mode 100644 src/lib/Address.js diff --git a/src/lib/Address.js b/src/lib/Address.js deleted file mode 100644 index d5e07e0..0000000 --- a/src/lib/Address.js +++ /dev/null @@ -1,75 +0,0 @@ -const secp256k1 = require("bcrypto/lib/secp256k1") -const KeyRing = require("bcoin/lib/primitives").KeyRing -const Script = require("bcoin/lib/script").Script - -/** - * Network type enumeration. - */ -const Network = Object.freeze({ mainnet: 1, testnet: 2, simnet: 4 }) - -/** - * Converts public key to bitcoin Witness Public Key Hash Address according to - * [BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki). - * @param {string} publicKey Public key as a hexadecimal representation of - * 64-byte concatenation of x and y coordinates. - * @param {Network} network Network for which address has to be calculated. - * @return {string} Bitcoin's P2WPKH address for given network. - */ -function publicKeyToP2WPKHaddress(publicKey, network) { - // Witness program requires usage of compressed public keys. - const compress = true - - const publicKeyBytes = Buffer.from(publicKey, "hex") - const publicKeyBCOIN = secp256k1.publicKeyImport(publicKeyBytes, compress) - - const keyRing = KeyRing.fromKey(publicKeyBCOIN, compress) - - const p2wpkhScript = Script.fromProgram(0, keyRing.getKeyHash()) - - const address = p2wpkhScript.getAddress() - - // Serialize address to a format specific to given network. - return address.toString(networkToBCOINvalue(network)) -} - -/** - * Converts bitcoin address to a script (ScriptPubKey). - * @param {string} address Bitcoin address. - * @return {string} Script. - */ -function addressToScript(address) { - return Script.fromAddress(address) - .toRaw() - .toString("hex") -} - -/** - * Converts network type from enumeration to a respective value used in `bcoin` - * library. - * @param {Network} network Network value from `Network` enumeration. - * @return {string} Network type used in `bcoin` library. - */ -function networkToBCOINvalue(network) { - switch (network) { - case Network.mainnet: - return "main" - case Network.testnet: - return "testnet" - case Network.simnet: - return "simnet" - default: - throw new Error( - `unsupported network [${networkType}], use one of: [${Object.keys( - network - ).map(key => { - return "Network." + key - })}]` - ) - } -} - -module.exports = { - Network, - publicKeyToP2WPKHaddress, - addressToScript -} From 3e328e5189a16b78d8414ed1ede304bf29e4bc9b Mon Sep 17 00:00:00 2001 From: Albert Date: Tue, 18 Aug 2020 02:42:59 +0200 Subject: [PATCH 4/8] Finish adding types --- package.json | 1 + src/BitcoinHelpers.ts | 8 +- src/CommonTypes.ts | 47 ++++++++++++ src/Constants.ts | 56 +++++++++----- src/Deposit.ts | 170 ++++++++++++++++++++++++++++------------- src/EthereumHelpers.ts | 43 +++++++---- src/Redemption.ts | 59 +++++++++----- src/TBTC.ts | 8 +- src/augmented.d.ts | 7 ++ src/lib.d.ts | 19 ++++- tsconfig.json | 1 + 11 files changed, 303 insertions(+), 116 deletions(-) create mode 100644 src/CommonTypes.ts create mode 100644 src/augmented.d.ts diff --git a/package.json b/package.json index 8685045..4e7fe45 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "index.js" ], "scripts": { + "build": "tsc", "test": "mocha --timeout 10000", "lint": "npm run lint:js", "lint:fix": "npm run lint:fix:js", diff --git a/src/BitcoinHelpers.ts b/src/BitcoinHelpers.ts index aac9821..588bf63 100644 --- a/src/BitcoinHelpers.ts +++ b/src/BitcoinHelpers.ts @@ -6,11 +6,11 @@ import {Script} from "bcoin/lib/script/index.js" import { BitcoinSPV } from "./lib/BitcoinSPV.js" /** @typedef { import("./lib/BitcoinSPV.js").Proof } Proof */ -import {Proof} from './lib/BitcoinSPV' +import type {Proof} from './lib/BitcoinSPV' import { BitcoinTxParser } from "./lib/BitcoinTxParser.js" import ElectrumClient from "./lib/ElectrumClient.js" /** @typedef { import("./lib/ElectrumClient.js").Config } ElectrumConfig */ -import {Config as ElectrumConfig} from "./lib/ElectrumClient" +import type {Config as ElectrumConfig} from "./lib/ElectrumClient" import BN from "bn.js" @@ -21,7 +21,7 @@ const BitcoinNetwork = { SIMNET: "simnet" } -type BitcoinNetworkType = "testnet" |"main" | "simnet" +export type BitcoinNetworkType = "testnet" |"main" | "simnet" /** * Found transaction details. @@ -31,7 +31,7 @@ type BitcoinNetworkType = "testnet" |"main" | "simnet" * @property {number} outputPosition Position of output in the transaction. * @property {number} value Value of the output (satoshis). */ -interface FoundTransaction{ +export interface FoundTransaction{ transactionID:string, outputPosition:number, value:number, diff --git a/src/CommonTypes.ts b/src/CommonTypes.ts new file mode 100644 index 0000000..bac65f7 --- /dev/null +++ b/src/CommonTypes.ts @@ -0,0 +1,47 @@ +// Some type definitions will be hosted here to avoid circular dependency errors +import type Web3 from 'web3' +import {BitcoinNetworkType} from './BitcoinHelpers' +import type {Config as ElectrumConfig} from "./lib/ElectrumClient" +import type {FoundTransaction} from './BitcoinHelpers' +import type {Contract} from 'web3-eth-contract' +import type BN from "bn.js" + + + export interface TBTCConfig { + web3:Web3, + bitcoinNetwork:BitcoinNetworkType, + electrum:ElectrumConfig, + } + + export interface RedemptionDetails{ + utxoValue:BN, + redeemerOutputScript:string, + requestedFee:BN, + outpoint:string, + digest:string + } + +export interface KeyPoint { + x:HexString, + y:HexString +} + + export interface DepositBaseClass { + address:string; + keepContract:Contract; + publicKeyPoint: Promise + getCurrentState():Promise + factory:any; + contract:Contract; + constructFundingProof(bitcoinTransaction:Omit, confirmations:number):Promise<[ + Buffer, + Buffer, + Buffer, + Buffer, + number, + Buffer, + string, + Buffer + ]> + getLatestRedemptionDetails():Promise + } \ No newline at end of file diff --git a/src/Constants.ts b/src/Constants.ts index a1643f0..cb9fe19 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -1,18 +1,20 @@ import TBTCConstantsJSON from "@keep-network/tbtc/artifacts/TBTCConstants.json" import EthereumHelpers from "./EthereumHelpers.js" -import web3Utils from "web3-utils" -const { toBN } = web3Utils +import { toBN } from "web3-utils" +import type BN from "bn.js" /** @typedef { import("web3").default.Web3.eth.Contract } Contract */ +import type {Contract} from 'web3-eth-contract' +import type {TBTCConfig} from './CommonTypes' class Constants { /** * @param {TBTCConfig} config The config to use for this constants instance. * @return {Constants} The TBTC constants. */ - static async withConfig(config) { + static async withConfig(config:TBTCConfig) { const { web3 } = config - const networkId = await web3.eth.net.getId() + const networkId = String(await web3.eth.net.getId()) const tbtcConstantsContract = EthereumHelpers.getDeployedContract( TBTCConstantsJSON, web3, @@ -44,7 +46,7 @@ class Constants { const calls = members.map(([constantGetter, constantName]) => { const call = tbtcConstantsContract.methods[constantGetter]().call return new Promise((resolve, reject) => { - const request = call.request(null, (error, value) => { + const request = call.request(null, (error:Error, value:any) => { if (error) { reject(error) } else { @@ -66,38 +68,50 @@ class Constants { return new Constants(constants, tbtcConstantsContract) } - constructor(constants, contract) { + public BENEFICIARY_REWARD_DIVISOR:BN; + public SATOSHI_MULTIPLIER:BN; + public DEPOSIT_TERM:BN; + public TX_PROOF_DIFFICULTY_FACTOR:BN; + public REDEMPTION_SIGNATURE_TIMEOUT:BN; + public INCREASE_FEE_TIMER:BN; + public REDEMPTION_PROOF_TIMEOUT:BN; + public MINIMUM_REDEMPTION_FEE:BN; + public FUNDING_PROOF_TIMEOUT:BN; + public SIGNING_GROUP_FORMATION_TIMEOUT:BN; + public COURTESY_CALL_DURATION:BN; + public AUCTION_DURATION:BN; + public PERMITTED_FEE_BUMPS:BN; + + constructor(constants:any, public contract:Contract) { /** @type {Contract} */ this.contract = contract - Object.assign(this, constants) - /** @type {BN} */ - this.BENEFICIARY_REWARD_DIVISOR + this.BENEFICIARY_REWARD_DIVISOR = constants.BENEFICIARY_REWARD_DIVISOR /** @type {BN} */ - this.SATOSHI_MULTIPLIER + this.SATOSHI_MULTIPLIER = constants.SATOSHI_MULTIPLIER /** @type {BN} */ - this.DEPOSIT_TERM + this.DEPOSIT_TERM = constants.DEPOSIT_TERM /** @type {BN} */ - this.TX_PROOF_DIFFICULTY_FACTOR + this.TX_PROOF_DIFFICULTY_FACTOR = constants.TX_PROOF_DIFFICULTY_FACTOR /** @type {BN} */ - this.REDEMPTION_SIGNATURE_TIMEOUT + this.REDEMPTION_SIGNATURE_TIMEOUT = constants.REDEMPTION_SIGNATURE_TIMEOUT /** @type {BN} */ - this.INCREASE_FEE_TIMER + this.INCREASE_FEE_TIMER = constants.INCREASE_FEE_TIMER /** @type {BN} */ - this.REDEMPTION_PROOF_TIMEOUT + this.REDEMPTION_PROOF_TIMEOUT = constants.REDEMPTION_PROOF_TIMEOUT /** @type {BN} */ - this.MINIMUM_REDEMPTION_FEE + this.MINIMUM_REDEMPTION_FEE = constants.MINIMUM_REDEMPTION_FEE /** @type {BN} */ - this.FUNDING_PROOF_TIMEOUT + this.FUNDING_PROOF_TIMEOUT = constants.FUNDING_PROOF_TIMEOUT /** @type {BN} */ - this.SIGNING_GROUP_FORMATION_TIMEOUT + this.SIGNING_GROUP_FORMATION_TIMEOUT = constants.SIGNING_GROUP_FORMATION_TIMEOUT /** @type {BN} */ - this.COURTESY_CALL_DURATION + this.COURTESY_CALL_DURATION = constants.COURTESY_CALL_DURATION /** @type {BN} */ - this.AUCTION_DURATION + this.AUCTION_DURATION = constants.AUCTION_DURATION /** @type {BN} */ - this.PERMITTED_FEE_BUMPS + this.PERMITTED_FEE_BUMPS = constants.PERMITTED_FEE_BUMPS } } diff --git a/src/Deposit.ts b/src/Deposit.ts index 414fbc2..838fd9b 100644 --- a/src/Deposit.ts +++ b/src/Deposit.ts @@ -1,11 +1,13 @@ import EventEmitter from "events" -import BitcoinHelpers from "./BitcoinHelpers.js" +import BitcoinHelpers, {FoundTransaction as BitcoinTransaction} from "./BitcoinHelpers.js" /** @typedef { import("./BitcoinHelpers.js").FoundTransaction } BitcoinTransaction */ import EthereumHelpers from "./EthereumHelpers.js" /** @typedef { import("web3").default.Web3.eth.Contract } Contract */ +import type {Contract} from 'web3-eth-contract' +import type {AbiItem} from 'web3-utils' import Redemption from "./Redemption.js" @@ -20,8 +22,10 @@ import VendingMachineJSON from "@keep-network/tbtc/artifacts/VendingMachine.json import FundingScriptJSON from "@keep-network/tbtc/artifacts/FundingScript.json" import BondedECDSAKeepJSON from "@keep-network/keep-ecdsa/artifacts/BondedECDSAKeep.json" -import web3Utils from "web3-utils" -const { toBN } = web3Utils +import {toBN} from "web3-utils" +import type BN from 'bn.js' +import type {TBTCConfig, DepositBaseClass, KeyPoint, RedemptionDetails} from './CommonTypes' + /** @typedef { import("bn.js") } BN */ /** @typedef { import("./TBTC").TBTCConfig } TBTCConfig */ @@ -59,7 +63,7 @@ export class DepositFactory { * * @param {TBTCConfig} config The config to use for this factory. */ - static async withConfig(config) { + static async withConfig(config:TBTCConfig) { const statics = new DepositFactory(config) await statics.resolveContracts() @@ -68,10 +72,11 @@ export class DepositFactory { return statics } + public State: typeof DepositStates /** * @param {TBTCConfig} config The config to use for this factory. */ - constructor(config) { + constructor(public config:TBTCConfig) { /** @package */ this.config = config @@ -101,7 +106,7 @@ export class DepositFactory { * * @return {Promise} The new deposit with the given lot size. */ - async withSatoshiLotSize(satoshiLotSize) { + async withSatoshiLotSize(satoshiLotSize:BN) { const isLotSizeAllowed = await this.systemContract.methods .isAllowedLotSize(satoshiLotSize.toString()) .call() @@ -126,10 +131,20 @@ export class DepositFactory { * * @return {Promise} The deposit at the given address. */ - async withAddress(depositAddress) { + async withAddress(depositAddress:HexString) { return await Deposit.forAddress(this, depositAddress) } + public constantsContract!:Contract + public systemContract!:Contract + public tokenContract!:Contract + public depositTokenContract!:Contract + public feeRebateTokenContract!:Contract + public depositContract!:Contract + public depositLogContract!:Contract + public depositFactoryContract!:Contract + public vendingMachineContract!:Contract + public fundingScriptContract!:Contract // Await the deployed() functions of all contract dependencies. /** @private */ async resolveContracts() { @@ -146,15 +161,15 @@ export class DepositFactory { [DepositFactoryJSON, "depositFactoryContract"], [FundingScriptJSON, "fundingScriptContract"], [VendingMachineJSON, "vendingMachineContract"] - ] + ] as [any, string][] contracts.map(([artifact, propertyName]) => { const contract = EthereumHelpers.getDeployedContract( artifact, web3, - networkId + String(networkId) ) - this[propertyName] = contract + Object.assign(this, {[propertyName]: contract}) }) /** @@ -219,14 +234,18 @@ export class DepositFactory { * * @param {BN} lotSize The lot size to use, in satoshis. */ - async createNewDepositContract(lotSize) { + async createNewDepositContract(lotSize:BN) { const creationCost = toBN( await this.systemContract.methods.getNewDepositFeeEstimate().call() ) - const accountBalance = await this.config.web3.eth.getBalance( - this.config.web3.eth.defaultAccount - ) + const defaultAccount = this.config.web3.eth.defaultAccount + if(defaultAccount === null){ + throw new Error("No default account set on the web3 provider") + } + const accountBalance = toBN(await this.config.web3.eth.getBalance( + defaultAccount + )) if (creationCost.lt(accountBalance)) { throw new Error( @@ -263,13 +282,31 @@ export class DepositFactory { // Bitcoin address handlers are given the deposit's Bitcoin address. /** @typedef {(address: string)=>void} BitcoinAddressHandler */ +type BitcoinAddressHandler = (address: string)=>void // Active handlers are given the deposit that just entered the ACTIVE state. /** @typedef {(deposit: Deposit)=>void} ActiveHandler */ +type ActiveHandler = (deposit: Deposit)=>void + +type EthereumTransaction = any // EthereumTransaction is the result of sending a rtx with send() + interface AutoSubmitState { + fundingTransaction:Promise, + fundingConfirmations:Promise<{ transaction: BitcoinTransaction, requiredConfirmations: number }>, + proofTransaction:Promise, + mintedTBTC:Promise + } + + interface FundingConfirmations{ + transaction:BitcoinTransaction, + requiredConfirmations:number + } -export default class Deposit { +export default class Deposit implements DepositBaseClass { // factory/*: DepositFactory*/; + public factory: DepositFactory; // address/*: string*/; + public address: string; // keepContract/*: string*/; + public keepContract:Contract; // contract/*: any*/; // bitcoinAddress/*: Promise*/; @@ -279,7 +316,7 @@ export default class Deposit { * @param {DepositFactory} factory * @param {BN} satoshiLotSize */ - static async forLotSize(factory, satoshiLotSize) { + static async forLotSize(factory:DepositFactory, satoshiLotSize:BN) { console.debug( "Creating new deposit contract with lot size", satoshiLotSize.toString(), @@ -294,14 +331,14 @@ export default class Deposit { `keep at address ${keepAddress}...` ) const web3 = factory.config.web3 - const contract = new web3.eth.Contract(DepositJSON.abi, depositAddress) - contract.options.from = web3.eth.defaultAccount + const contract = new web3.eth.Contract(DepositJSON.abi as AbiItem[], depositAddress) + contract.options.from = web3.eth.defaultAccount ?? undefined contract.options.handleRevert = true const keepContract = new web3.eth.Contract( - BondedECDSAKeepJSON.abi, + BondedECDSAKeepJSON.abi as AbiItem[], keepAddress ) - keepContract.options.from = web3.eth.defaultAccount + keepContract.options.from = web3.eth.defaultAccount ?? undefined keepContract.options.handleRevert = true return new Deposit(factory, contract, keepContract) @@ -311,11 +348,11 @@ export default class Deposit { * @param {DepositFactory} factory * @param {string} address */ - static async forAddress(factory, address) { + static async forAddress(factory:DepositFactory, address:HexString) { console.debug(`Looking up Deposit contract at address ${address}...`) const web3 = factory.config.web3 - const contract = new web3.eth.Contract(DepositJSON.abi, address) - contract.options.from = web3.eth.defaultAccount + const contract = new web3.eth.Contract(DepositJSON.abi as AbiItem[], address) + contract.options.from = web3.eth.defaultAccount ?? undefined contract.options.handleRevert = true console.debug(`Looking up Created event for deposit ${address}...`) @@ -333,10 +370,10 @@ export default class Deposit { const keepAddress = createdEvent.returnValues._keepAddress console.debug(`Found keep address ${keepAddress}.`) const keepContract = new web3.eth.Contract( - BondedECDSAKeepJSON.abi, + BondedECDSAKeepJSON.abi as AbiItem[], keepAddress ) - keepContract.options.from = web3.eth.defaultAccount + keepContract.options.from = web3.eth.defaultAccount ?? undefined keepContract.options.handleRevert = true return new Deposit(factory, contract, keepContract) @@ -346,16 +383,21 @@ export default class Deposit { * @param {DepositFactory} factory * @param {any | string} tdt */ - static async forTDT(factory, tdt) { + static async forTDT(factory:DepositFactory, tdt:any | string) { return new Deposit(factory, "") } + public contract:Contract + public activeStatePromise:Promise + public publicKeyPoint:Promise + public bitcoinAddress:Promise + public receivedFundingConfirmationEmitter:EventEmitter /** * @param {DepositFactory} factory * @param {TruffleContract} depositContract * @param {TruffleContract} keepContract */ - constructor(factory, depositContract, keepContract) { + constructor(factory:DepositFactory, depositContract:Contract, keepContract:Contract) { if (!keepContract) { throw new Error("Keep contract required for Deposit instantiation.") } @@ -379,11 +421,12 @@ export default class Deposit { // /------------------------------- Accessors ------------------------------- + public _fundingTransaction?:Promise /** * Promise to when a Bitcoin funding transaction is found for the address. * @type {Promise} */ - get fundingTransaction() { + get fundingTransaction():Promise { // Lazily initalized. return (this._fundingTransaction = this._fundingTransaction || @@ -402,11 +445,12 @@ export default class Deposit { * @property {BitcoinTransaction} transaction * @property {number} requiredConfirmations */ + public _fundingConfirmations?:Promise /** * Promise to when the deposit funding transaction is sufficiently confirmed. * @type {Promise} */ - get fundingConfirmations() { + get fundingConfirmations():Promise { // Lazily initalized. return (this._fundingConfirmations = this._fundingConfirmations || @@ -510,7 +554,7 @@ export default class Deposit { * wallet. Note that exceptions in this handler are not managed, so * the handler itself should deal with its own failure possibilities. */ - onBitcoinAddressAvailable(bitcoinAddressHandler) { + onBitcoinAddressAvailable(bitcoinAddressHandler:BitcoinAddressHandler) { this.bitcoinAddress.then(bitcoinAddressHandler) } @@ -525,16 +569,18 @@ export default class Deposit { * so the handler itself should deal with its own failure * possibilities. */ - onActive(activeHandler) { + onActive(activeHandler:ActiveHandler) { this.activeStatePromise.then(() => { activeHandler(this) }) } - onReadyForProof(proofHandler /* : (prove)=>void*/) { + /* + onReadyForProof(proofHandler /* : (prove)=>void) { // prove(txHash) is a thing, will submit funding proof for the given // Bitcoin txHash; no verification initially. } + */ /** * Registers a handler for notification when the Bitcoin funding transaction @@ -544,7 +590,10 @@ export default class Deposit { * A handler that receives an object with the transactionID and * confirmations as its parameter. */ - onReceivedFundingConfirmation(onReceivedFundingConfirmationHandler) { + onReceivedFundingConfirmation(onReceivedFundingConfirmationHandler:(fundingConfirmation:{ + transactionID:string, + confirmations:number + })=>void) { this.receivedFundingConfirmationEmitter.on( "receivedFundingConfirmation", onReceivedFundingConfirmationHandler @@ -668,7 +717,7 @@ export default class Deposit { "Transfer" ) - return toBN(transferEvent.value).div(toBN(10).pow(18)) + return toBN(transferEvent.value).div(toBN(10).pow(toBN(18))) } /** @@ -730,7 +779,7 @@ export default class Deposit { * redeemer address, and a redemption request from a party that has * insufficient TBTC to redeem. */ - async requestRedemption(redeemerAddress) { + async requestRedemption(redeemerAddress:string):Promise { const inVendingMachine = await this.inVendingMachine() const thisAccount = this.factory.config.web3.eth.defaultAccount const owner = await this.getOwner() @@ -827,7 +876,9 @@ export default class Deposit { this.factory.systemContract, "RedemptionRequested" ) - const redemptionDetails = this.redemptionDetailsFromEvent(redemptionRequest) + // Casted to to get around a deficiency of Typescript + // See https://github.com/microsoft/TypeScript/issues/15300 + const redemptionDetails = this.redemptionDetailsFromEvent(redemptionRequest as any) return new Redemption(this, redemptionDetails) } @@ -839,7 +890,7 @@ export default class Deposit { * Returns a promise to the redemption details, or to null if there is no * current redemption in progress. */ - async getLatestRedemptionDetails() { + async getLatestRedemptionDetails():Promise { // If the contract is ACTIVE, there's definitely no redemption. This can // be generalized to a state check that the contract is either // AWAITING_WITHDRAWAL_SIGNATURE or AWAITING_WITHDRAWAL_PROOF, but let's @@ -858,7 +909,8 @@ export default class Deposit { return null } - return this.redemptionDetailsFromEvent(redemptionRequest.returnValues) + // See other calls to redemptionDetailsFromEvent for rational on casting to + return this.redemptionDetailsFromEvent(redemptionRequest.returnValues as any) } // /------------------------------- Helpers --------------------------------- @@ -870,6 +922,7 @@ export default class Deposit { * @prop {Promise} proofTransaction * @prop {Promise} mintedTBTC */ + public autoSubmittingState?:AutoSubmitState /** * This method enables the deposit's auto-submission capabilities. In * auto-submit mode, the deposit will automatically monitor for a new @@ -892,13 +945,13 @@ export default class Deposit { * rejected, and they are in a sequence where later promises will be * rejected by earlier ones. */ - autoSubmit() { + autoSubmit():AutoSubmitState { // Only enable auto-submitting once. if (this.autoSubmittingState) { return this.autoSubmittingState } /** @type {AutoSubmitState} */ - const state = (this.autoSubmittingState = {}) + const state = (this.autoSubmittingState = {} as AutoSubmitState) state.fundingTransaction = this.fundingTransaction state.fundingConfirmations = this.fundingConfirmations @@ -924,13 +977,13 @@ export default class Deposit { return state } - autoMint() { + autoMint():AutoSubmitState { // Only enable auto-submitting once. if (this.autoSubmittingState) { return this.autoSubmittingState } /** @type {AutoSubmitState} */ - const state = (this.autoSubmittingState = {}) + const state = (this.autoSubmittingState = {} as AutoSubmitState) state.fundingTransaction = this.fundingTransaction state.fundingConfirmations = this.fundingConfirmations @@ -968,7 +1021,7 @@ export default class Deposit { ) console.debug(`Minted`, transferEvent.value, `TBTC.`) - return toBN(transferEvent.value).div(toBN(10).pow(18)) + return toBN(transferEvent.value).div(toBN(10).pow(toBN(18))) } ) @@ -986,7 +1039,7 @@ export default class Deposit { // // Returns a promise that will be fulfilled once the public key is // available, with a public key point with x and y properties. - async findOrWaitForPublicKeyPoint() { + async findOrWaitForPublicKeyPoint():Promise { const signerPubkeyEvent = await this.readPublishedPubkeyEvent() if (signerPubkeyEvent) { console.debug( @@ -1062,7 +1115,7 @@ export default class Deposit { ) } - async publicKeyPointToBitcoinAddress(publicKeyPoint) { + async publicKeyPointToBitcoinAddress(publicKeyPoint:KeyPoint) { return BitcoinHelpers.Address.publicKeyPointToP2WPKHAddress( publicKeyPoint.x, publicKeyPoint.y, @@ -1086,7 +1139,7 @@ export default class Deposit { // // Constructed this way to serve both qualify + mint and simple // qualification flows. - async constructFundingProof(bitcoinTransaction, confirmations) { + async constructFundingProof(bitcoinTransaction:Omit, confirmations:number) { const { transactionID, outputPosition } = bitcoinTransaction const { parsedTransaction, @@ -1109,12 +1162,27 @@ export default class Deposit { Buffer.from(merkleProof, "hex"), txInBlockIndex, Buffer.from(chainHeaders, "hex") + ] as [ + Buffer, + Buffer, + Buffer, + Buffer, + number, + Buffer, + string, + Buffer ] } redemptionDetailsFromEvent( - redemptionRequestedEventArgs - ) /* : RedemptionDetails*/ { + redemptionRequestedEventArgs:{ + _utxoValue:string, + _redeemerOutputScript:string, + _requestedFee:string, + _outpoint:string, + _digest:string + } + ):RedemptionDetails /* : RedemptionDetails*/ { const { _utxoValue, _redeemerOutputScript, @@ -1314,7 +1382,7 @@ export default class Deposit { * @param {string} digest Digest to check approval for. * @return {boolean} True if signature approved, false if not (fraud). */ - async wasSignatureApproved(digest) { + async wasSignatureApproved(digest:string) { const events = await this.keepContract.getPastEvents("SignatureRequested", { fromBlock: 0, toBlock: "latest", @@ -1332,7 +1400,7 @@ export default class Deposit { * @param {*} signedDigest The digest signed by the signature vrs tuple. * @param {*} preimage The sha256 preimage of the digest. */ - async provideFundingECDSAFraudProof(v, r, s, signedDigest, preimage) { + async provideFundingECDSAFraudProof(v:number, r:HexString, s:HexString, signedDigest:HexString, preimage:HexString) { await EthereumHelpers.sendSafely( this.contract.methods.provideFundingECDSAFraudProof( v, @@ -1353,7 +1421,7 @@ export default class Deposit { * @param {*} signedDigest The digest signed by the signature vrs tuple. * @param {*} preimage The sha256 preimage of the digest. */ - async provideECDSAFraudProof(v, r, s, signedDigest, preimage) { + async provideECDSAFraudProof(v:number, r:HexString, s:HexString, signedDigest:HexString, preimage:HexString) { await EthereumHelpers.sendSafely( this.contract.methods.provideECDSAFraudProof( v, diff --git a/src/EthereumHelpers.ts b/src/EthereumHelpers.ts index 9a8c746..1c78992 100644 --- a/src/EthereumHelpers.ts +++ b/src/EthereumHelpers.ts @@ -1,4 +1,7 @@ /** @typedef { import("web3").default } Web3 */ +import type Web3 from 'web3' +import type {Contract, EventOptions, ContractSendMethod, SendOptions, EventData} from 'web3-eth-contract' +import type {TransactionReceipt} from 'web3-core' /** * Checks whether the given web3 instance is connected to Ethereum mainnet. @@ -7,7 +10,7 @@ * @return {Promise} True if the web3 instance is aimed at Ethereum * mainnet, false otherwise. */ -async function isMainnet(web3) { +async function isMainnet(web3:Web3) { return (await web3.eth.getChainId()) == 0x1 } @@ -24,23 +27,26 @@ async function isMainnet(web3) { * @return {Object} A key-value dictionary of the event's parameters. */ function readEventFromTransaction( - web3, - transaction, - sourceContract, - eventName + web3:Web3, + transaction:TransactionReceipt, + sourceContract:Contract, + eventName:string ) { const eventABI = sourceContract.options.jsonInterface.find( entry => entry.type == "event" && entry.name == eventName ) + if(eventABI === undefined){ + throw new Error(`Event ${eventName} could not be found in transaction ${transaction.transactionHash}`) + } - return Object.values(transaction.events) + return Object.values(transaction.events?? []) .filter( event => event.address == sourceContract.options.address && - event.raw.topics[0] == eventABI.signature + event?.raw?.topics[0] == (eventABI as any).signature ) .map(_ => - web3.eth.abi.decodeLog(eventABI.inputs, _.raw.data, _.raw.topics.slice(1)) + web3.eth.abi.decodeLog(eventABI.inputs!, _.raw!.data, _.raw!.topics.slice(1)) )[0] } @@ -56,7 +62,7 @@ function readEventFromTransaction( * @return {Promise} A promise that will be fulfilled by the event * object once it is received. */ -function getEvent(sourceContract, eventName, filter) { +function getEvent(sourceContract:Contract, eventName:string, filter?:EventOptions['filter']):Promise { return new Promise((resolve, reject) => { // As a workaround for a problem with MetaMask version 7.1.1 where subscription // for events doesn't work correctly we pull past events in a loop until @@ -84,7 +90,7 @@ function getEvent(sourceContract, eventName, filter) { }) } -async function getExistingEvent(source, eventName, filter) { +async function getExistingEvent(source:Contract, eventName:string, filter?:EventOptions['filter']) { const events = await source.getPastEvents(eventName, { fromBlock: 0, toBlock: "latest", @@ -100,7 +106,7 @@ async function getExistingEvent(source, eventName, filter) { * @param {string} bytesString An Ethereum-encoded `bytes` string * @return {string} The hexadecimal string. */ -function bytesToRaw(bytesString) { +function bytesToRaw(bytesString:HexString):string { return bytesString.replace("0x", "").slice(2) } @@ -123,7 +129,7 @@ function bytesToRaw(bytesString) { * method. Fails the promise if gas estimation fails, extracting an * on-chain error if possible. */ -async function sendSafely(boundContractMethod, sendParams, forceSend) { +async function sendSafely(boundContractMethod:ContractSendMethod&{call(params:any):any}, sendParams?:SendOptions, forceSend:boolean = false):Promise { try { // Clone `sendParams` so we aren't exposed to providers that modify `sendParams`. const gasEstimate = await boundContractMethod.estimateGas({ ...sendParams }) @@ -164,6 +170,13 @@ async function sendSafely(boundContractMethod, sendParams, forceSend) { } } +type Artifact = { + contractName:string, + abi:any, + networks:{[netId:string]:{ + address:string + }} +} /** * Wraps the {@link sendSafely} method with a retry logic. * @see {@link sendSafely} @@ -219,8 +232,8 @@ async function sendSafelyRetryable( * @param {string} networkId * @return {Contract} */ -function getDeployedContract(artifact, web3, networkId) { - function lookupAddress(artifact) { +function getDeployedContract(artifact:Artifact, web3:Web3, networkId:string) { + function lookupAddress(artifact:Artifact) { const deploymentInfo = artifact.networks[networkId] if (!deploymentInfo) { throw new Error( @@ -232,7 +245,7 @@ function getDeployedContract(artifact, web3, networkId) { const contract = new web3.eth.Contract(artifact.abi) contract.options.address = lookupAddress(artifact) - contract.options.from = web3.eth.defaultAccount + contract.options.from = web3.eth.defaultAccount ?? undefined contract.options.handleRevert = true return contract diff --git a/src/Redemption.ts b/src/Redemption.ts index 09098e0..974e38f 100644 --- a/src/Redemption.ts +++ b/src/Redemption.ts @@ -4,8 +4,9 @@ import BitcoinHelpers from "./BitcoinHelpers.js" import EthereumHelpers from "./EthereumHelpers.js" -import web3Utils from "web3-utils" -const { toBN } = web3Utils +import {toBN} from "web3-utils" + +import type {DepositBaseClass, RedemptionDetails} from './CommonTypes' /** * Details of a given redemption at a given point in time. @@ -17,12 +18,22 @@ const { toBN } = web3Utils * @property {Buffer} digest The raw digest bytes. */ +interface AutoSubmitState { + broadcastTransactionID:Promise, + confirmations: Promise<{ transactionID: string, requiredConfirmations: number }>, + proofTransaction:Promise +} + /** * Details of a given unsigned transaction * @typedef {Object} UnsignedTransactionDetails * @property {string} hex The raw transaction hex string. - * @property {digest} digest The transaction's digest. + * @property {Buffer} digest The transaction's digest. */ +interface UnsignedTransactionDetails { + hex:HexString, + digest:string +} /** * The Redemption class encapsulates the operations that finalize an already- @@ -42,16 +53,22 @@ const { toBN } = web3Utils */ export default class Redemption { // deposit/*: Deposit*/ + public deposit:DepositBaseClass // redemptionDetails/*: Promise*/ - // unsignedTransaction/*: Promise*/ + public redemptionDetails: Promise + // unsignedTransactionDetails/*: Promise*/ + public unsignedTransactionDetails: Promise // signedTransaction/*: Promise*/ + public signedTransaction: Promise // withdrawnEmitter/*: EventEmitter*/ + public withdrawnEmitter: EventEmitter + public receivedConfirmationEmitter: EventEmitter constructor( - deposit /* : Deposit*/, - redemptionDetails /* : RedemptionDetails?*/ + deposit:DepositBaseClass /* : Deposit*/, + redemptionDetails?:RedemptionDetails /* : RedemptionDetails?*/ ) { this.deposit = deposit this.withdrawnEmitter = new EventEmitter() @@ -136,10 +153,11 @@ export default class Redemption { /** * @typedef {Object} AutoSubmitState - * @prop {Promise} signedTransaction - * @prop {Promise<{ transaction: FoundTransaction, requiredConfirmations: Number }>} confirmations + * @prop {Promise} broadcastTransactionID + * @prop {Promise<{ transactionID: string, requiredConfirmations: Number }>} confirmations * @prop {Promise} proofTransaction */ + public autoSubmittingState?:AutoSubmitState; /** * This method enables the redemption's auto-submission capabilities. * @@ -151,12 +169,12 @@ export default class Redemption { * rejected, and they are in a sequence where later promises will be * rejected by earlier ones. */ - autoSubmit() { + autoSubmit():AutoSubmitState { if (this.autoSubmittingState) { return this.autoSubmittingState } - const state = (this.autoSubmittingState = {}) + const state = (this.autoSubmittingState = {} as AutoSubmitState) state.broadcastTransactionID = this.signedTransaction.then( async signedTransaction => { @@ -181,11 +199,12 @@ export default class Redemption { `Broadcasting signed redemption transaction to Bitcoin chain ` + `for deposit ${this.deposit.address}...` ) - transaction = await BitcoinHelpers.Transaction.broadcast( + return (await BitcoinHelpers.Transaction.broadcast( signedTransaction - ) + )).transactionID + } else { + return transaction.transactionID } - return transaction.transactionID } ) @@ -240,7 +259,7 @@ export default class Redemption { * the proof; if this is not provided, looks up the required * confirmations via the deposit. */ - async proveWithdrawal(transactionID, confirmations) { + async proveWithdrawal(transactionID:HexString, confirmations:number) { if (!confirmations) { // 0 still triggers a lookup confirmations = ( @@ -251,7 +270,7 @@ export default class Redemption { const provableTransaction = { transactionID: transactionID, // For filtering, see provideRedemptionProof call below. - outputPosition: "output position" + outputPosition: Number.NEGATIVE_INFINITY } const proofArgs = await this.deposit.constructFundingProof( provableTransaction, @@ -265,18 +284,18 @@ export default class Redemption { // However, constructFundingProof includes it for deposit funding // proofs. Here, we filter it out to produce the right set of // parameters. - ...proofArgs.filter(_ => _ != "output position") + ...proofArgs.filter((_:Buffer|number|string) => _ !== Number.NEGATIVE_INFINITY) ) ) this.withdrawnEmitter.emit("withdrawn", transactionID) } - onBitcoinTransactionSigned(transactionHandler /* : (transaction)=>void*/) { + onBitcoinTransactionSigned(transactionHandler : (transaction:string)=>void) { this.signedTransaction.then(transactionHandler) } - onWithdrawn(withdrawalHandler /* : (txHash)=>void*/) { + onWithdrawn(withdrawalHandler : (txHash:HexString)=>void) { // bitcoin txHash this.withdrawnEmitter.on("withdrawn", withdrawalHandler) } @@ -289,7 +308,7 @@ export default class Redemption { * A handler that passes an object with the transactionID and * confirmations as its parameter */ - onReceivedConfirmation(onReceivedConfirmationHandler) { + onReceivedConfirmation(onReceivedConfirmationHandler: (transactionID:HexString, confirmations:number)=>void) { this.receivedConfirmationEmitter.on( "receivedConfirmation", onReceivedConfirmationHandler @@ -307,7 +326,7 @@ export default class Redemption { * details, or the result of looking these up on-chain if none were * past. */ - async getLatestRedemptionDetails(existingRedemptionDetails) { + async getLatestRedemptionDetails(existingRedemptionDetails:RedemptionDetails|undefined):Promise { if (existingRedemptionDetails) { return existingRedemptionDetails } diff --git a/src/TBTC.ts b/src/TBTC.ts index 04fd148..b259776 100644 --- a/src/TBTC.ts +++ b/src/TBTC.ts @@ -17,6 +17,7 @@ import TBTCSystemJSON from "@keep-network/tbtc/artifacts/TBTCSystem.json" * @property {BitcoinNetwork} bitcoinNetwork * @property {ElectrumConfig} electrum */ +import type {TBTCConfig} from './CommonTypes' /** * The entry point to the TBTC system. Call `TBTC.withConfig()` and pass the @@ -34,7 +35,7 @@ export class TBTC { * configured Bitcoin and Ethereum networks are either both mainnet or * both testnet. */ - static async withConfig(config, networkMatchCheck = true) { + static async withConfig(config:TBTCConfig, networkMatchCheck = true) { const ethereumMainnet = await EthereumHelpers.isMainnet(config.web3) const bitcoinMainnet = config.bitcoinNetwork == BitcoinHelpers.Network.MAINNET @@ -58,13 +59,14 @@ export class TBTC { return new TBTC(depositFactory, constants, config) } + public satoshisPerTbtc:BN /** * * @param {DepositFactory} depositFactory * @param {Constants} constants * @param {TBTCConfig} config */ - constructor(depositFactory, constants, config) { + constructor(public depositFactory:DepositFactory, public constants:Constants, public config:TBTCConfig) { /** @package */ this.depositFactory = depositFactory /** @package */ @@ -91,7 +93,7 @@ export default { * @param {TBTCConfig} config * @param {boolean} networkMatchCheck */ - withConfig: async (config, networkMatchCheck = true) => { + withConfig: async (config:TBTCConfig, networkMatchCheck = true) => { return await TBTC.withConfig(config, networkMatchCheck) }, BitcoinNetwork: BitcoinHelpers.Network diff --git a/src/augmented.d.ts b/src/augmented.d.ts new file mode 100644 index 0000000..b10ab93 --- /dev/null +++ b/src/augmented.d.ts @@ -0,0 +1,7 @@ +import type {Options} from 'web3-eth-contract' + +declare module 'web3-eth-contract' { + export interface Options { + handleRevert:boolean + } +} \ No newline at end of file diff --git a/src/lib.d.ts b/src/lib.d.ts index 1e91406..1c5e9f0 100644 --- a/src/lib.d.ts +++ b/src/lib.d.ts @@ -36,6 +36,7 @@ declare module "bcoin/lib/bcoin-browser.js" { })):Output } } + declare module "bcrypto/lib/secp256k1.js" { class ECDSA{ get size():number; @@ -45,7 +46,7 @@ declare module "bcrypto/lib/secp256k1.js" { const ECDSAimpl:ECDSA export default ECDSAimpl; } -declare module "bcrypto/lib/internal/signature.js" {} + declare module "bcoin/lib/primitives/index.js" { export class KeyRing{ static fromKey(key:Buffer, compress?:boolean):KeyRing; @@ -55,6 +56,7 @@ declare module "bcoin/lib/primitives/index.js" { getKeyHash():Buffer; } } + declare module "bcoin/lib/script/index.js" { type network = `main`|`testnet`|`regtest`|`segnet4`|'simnet' class Address{ @@ -80,4 +82,17 @@ declare module "bcrypto/lib/internal/signature.js" { } } -type HexString=string \ No newline at end of file +type HexString=string + +/* +declare module "@keep-network/tbtc/artifacts/TBTCConstants.json" { const any:any; export default any; } +declare module "@keep-network/tbtc/artifacts/TBTCSystem.json"{ const any:any; export default any; } +declare module "@keep-network/tbtc/artifacts/TBTCDepositToken.json"{ const any:any; export default any; } +declare module "@keep-network/tbtc/artifacts/Deposit.json"{ const any:any; export default any; } +declare module "@keep-network/tbtc/artifacts/DepositFactory.json"{ const any:any; export default any; } +declare module "@keep-network/tbtc/artifacts/TBTCToken.json"{ const any:any; export default any; } +declare module "@keep-network/tbtc/artifacts/FeeRebateToken.json"{ const any:any; export default any; } +declare module "@keep-network/tbtc/artifacts/VendingMachine.json"{ const any:any; export default any; } +declare module "@keep-network/tbtc/artifacts/FundingScript.json"{ const any:any; export default any; } +declare module "@keep-network/keep-ecdsa/artifacts/BondedECDSAKeep.json"{ const any:any; export default any; } +*/ \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 87f41be..3add16f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, "declaration": true, + "resolveJsonModule": true, "noUnusedParameters": true }, "exclude": ["node_modules", "build"] From 1f0862a0d1aa062d605fd7f32a813921f371b734 Mon Sep 17 00:00:00 2001 From: Albert Date: Tue, 18 Aug 2020 02:47:21 +0200 Subject: [PATCH 5/8] Make eslint ignore build artifacts --- .eslintignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a007fea --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +build/* From 7ec6c11cab88dfe336b20eea0be62339e4e58dff Mon Sep 17 00:00:00 2001 From: Albert Date: Tue, 18 Aug 2020 02:53:08 +0200 Subject: [PATCH 6/8] Adapt module info to new build system --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4e7fe45..2703d04 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "@keep-network/tbtc.js", "version": "0.18.0-rc", - "type": "module", "description": "tbtc.js provides JS bindings to the tBTC system that establishes a TBTC ERC20 token supply-pegged to BTC.", "repository": { "type": "git", @@ -11,11 +10,13 @@ "url": "https://github.com/keep-network/tbtc.js/issues" }, "homepage": "https://github.com/keep-network/tbtc.js", - "main": "index.js", + "main": "build/index.js", + "types": "build/index.d.ts", "bin": "./bin/tbtc.js", "files": [ "bin/", "src/", + "build/", "index.js" ], "scripts": { From 7da85e412efc34dd73f13388c05a0facadf9aa87 Mon Sep 17 00:00:00 2001 From: Albert Date: Tue, 18 Aug 2020 13:17:23 +0200 Subject: [PATCH 7/8] Solve safelySend type errors --- src/EthereumHelpers.ts | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/EthereumHelpers.ts b/src/EthereumHelpers.ts index 1c78992..3aca65f 100644 --- a/src/EthereumHelpers.ts +++ b/src/EthereumHelpers.ts @@ -1,7 +1,8 @@ /** @typedef { import("web3").default } Web3 */ import type Web3 from 'web3' -import type {Contract, EventOptions, ContractSendMethod, SendOptions, EventData} from 'web3-eth-contract' -import type {TransactionReceipt} from 'web3-core' +import type {Contract, EventOptions, EstimateGasOptions, EventData} from 'web3-eth-contract' +import type {TransactionReceipt, PromiEvent} from 'web3-core' +import type BN from 'bn.js' /** * Checks whether the given web3 instance is connected to Ethereum mainnet. @@ -14,6 +15,29 @@ async function isMainnet(web3:Web3) { return (await web3.eth.getChainId()) == 0x1 } +// Types modified from 'web3-eth-contract' because those types are wrong and can't be fixed +// through type augmentation +// 'from' should be optional as if it's not given web3 will use the from assigned to the contract +// See https://github.com/ethereum/web3.js/blob/01518219cd74fc1a016ca6615158fe52daa94722/packages/web3-eth-contract/src/index.js#L357 +interface ContractCallOptions { + from?: string; + gasPrice?: string; + gas?: number; + value?: number | string | BN; +} + +interface ContractCall { + send( + options: ContractCallOptions, + callback?: (err: Error, transactionHash: string) => void + ): PromiEvent; + estimateGas( + options: EstimateGasOptions, + callback?: (err: Error, gas: number) => void + ): Promise; + call(params:any):any; +} + /** * From a given transaction result, extracts the first event with the given * name from the given source contract. @@ -129,7 +153,7 @@ function bytesToRaw(bytesString:HexString):string { * method. Fails the promise if gas estimation fails, extracting an * on-chain error if possible. */ -async function sendSafely(boundContractMethod:ContractSendMethod&{call(params:any):any}, sendParams?:SendOptions, forceSend:boolean = false):Promise { +async function sendSafely(boundContractMethod:ContractCall, sendParams?:ContractCallOptions, forceSend:boolean = false):Promise { try { // Clone `sendParams` so we aren't exposed to providers that modify `sendParams`. const gasEstimate = await boundContractMethod.estimateGas({ ...sendParams }) @@ -158,7 +182,7 @@ async function sendSafely(boundContractMethod:ContractSendMethod&{call(params:an } if (forceSend) { - return boundContractMethod.send(sendParams) + return boundContractMethod.send(sendParams ?? {}) } else { // If we weren't able to get a better error from `call`, throw the // original exception. From 5578e0fecefa080745db08f7a8d0b6981d350c02 Mon Sep 17 00:00:00 2001 From: Albert Date: Tue, 18 Aug 2020 18:50:52 +0200 Subject: [PATCH 8/8] Add types on new changes --- src/EthereumHelpers.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EthereumHelpers.ts b/src/EthereumHelpers.ts index 3aca65f..cedaeb5 100644 --- a/src/EthereumHelpers.ts +++ b/src/EthereumHelpers.ts @@ -219,10 +219,10 @@ type Artifact = { * on-chain error if possible. */ async function sendSafelyRetryable( - boundContractMethod, - sendParams, - forceSend, - totalAttempts + boundContractMethod:ContractCall, + sendParams:ContractCallOptions, + forceSend:boolean, + totalAttempts:number ) { for (let attempt = 1; true; attempt++) { try {