Skip to content
This repository was archived by the owner on Mar 28, 2023. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ to the `Deposit.sol` file in the tBTC system.

We're listening. Hit us up at https://discord.gg/4R6RGFf.

If you are running in Node, depending on your Node version, you may need to add
the `--experimental-modules` and/or `--experimental-json-modules` flags to your
`node` invocation..

== License

This code is published under the MIT license. See the LICENSE file in this
Expand Down
237 changes: 237 additions & 0 deletions bin/tbtc-submitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#!/usr/bin/env node --experimental-modules
// @ts-check
const description = `
tbtc-submitter.js <ethereum-key-file> <bitcoin-wallet-file>
In default mode, creates a single tBTC deposit with the given Ethereum
key file and funds it using the Bitcoin wallet defined by the given
file.

-c,--continuous
This flag repeatedly creates tBTC deposits and funds them. Rather
than waiting for each deposit's funding transaction to fully
confirm, a new deposit opened once the previous deposit's signing
group becomes available.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth flagging, current code doesn't pay attention to a -c flag, and though it reads the two file parameters, it ignores them.

`

import Web3 from "web3"
import TBTC from "../index.js"
import UI from "./ui.js"

import BCoin from "bcoin"
const {
WalletDB,
Script,
MTX,
Coin,
} = BCoin.set('testnet')

import ProviderEngine from "web3-provider-engine"
import WebsocketSubprovider from "web3-provider-engine/subproviders/websocket.js"
import Subproviders from "@0x/subproviders"
import BitcoinHelpers from "../src/BitcoinHelpers.js";
import { DepositFactory } from "../src/Deposit.js"

/** @typedef { import("../src/BitcoinHelpers.js").FoundTransaction } FoundTransaction */
/**
* @typedef {Object} TransactionInfo
* @prop {any} [transaction]
*/
/**
* @typedef {TransactionInfo & FoundTransaction} FundingTransactionInfo
*/

const engine = new ProviderEngine({ pollingInterval: 1000 })
engine.addProvider(
// For address 0x420ae5d973e58bc39822d9457bf8a02f127ed473.
new Subproviders.PrivateKeyWalletSubprovider(
"b6252e08d7a11ab15a4181774fdd58689b9892fe9fb07ab4f026df9791966990",
),
)
engine.addProvider(
new WebsocketSubprovider(
{
rpcUrl: "wss://ropsten.infura.io/ws/v3/59fb36a36fa4474b890c13dd30038be5",
debug: false,
origin: '',
},
// "http://eth-tx-node.default.svc.cluster.local:8545/",
)
)

// -------------------------------- SETUP --------------------------------------
// @ts-ignore Web3 is declared as taking a `provider`, engine is a `Provider`.
const web3 = new Web3(engine)
engine.start()

// --------------------------------- ARGS --------------------------------------
let args = process.argv.slice(2)
if (process.argv[0].includes("tbtc-submitter.js")) {
args = process.argv.slice(1) // invoked directly, no node
}

const ethereumKeyFile = args[0]
const bitcoinWalletFile = args[1]

let remainingPromise = Promise.resolve(true)
/** @type {FundingTransactionInfo} */
let latestFundingInfo

/**
* @param {DepositFactory} Deposit Deposit factory object.
* @param {import("../src/Deposit.js").BN} lotSize The size of the deposit lot
* to create.
* @param { import("bcoin/lib/wallet/wallet.js") } wallet Bitcoin wallet for
* funding the deposit.
* @param {number} maxFee The highest fee to use for the Bitcoin funding
* transaction.
*
* @returns {()=>void} A function that, when called, will create a deposit of
* the passed lotSize using the passed deposit factory and fund it
* using the passed wallet.
*/
function fundDepositAndCreationFn(Deposit, lotSize, wallet, maxFee) {
const runner = async () => {
const deposit = await Deposit.withAddress("0x7C935d413A35c28C9e7b91b82c8B5bfDA57E4780") // withSatoshiLotSize(lotSize)

deposit
.onBitcoinAddressAvailable(async address => {
const receiveAddress = await wallet.receiveAddress()
const keyRing = await wallet.getPrivateKey(receiveAddress, "")

const lastTx = MTX.fromRaw(latestFundingInfo.transaction.hex, 'hex')
const outputIndex =
lastTx.outputs.findIndex(
(_) => {
return _.getAddress().toString(Deposit.config.bitcoinNetwork) ==
receiveAddress.toString(Deposit.config.bitcoinNetwork)
}
)

const fundingTransaction = new MTX()
fundingTransaction.addOutput({
script: Script.fromAddress(address),
value: lotSize.toNumber(),
})
let feePerKb
try {
feePerKb = await BitcoinHelpers.Transaction.estimateFeePerKb()
} catch(_) {
// Leave feePerKb null if we couldn't estimate.
}
await fundingTransaction.fund(
[Coin.fromTX(lastTx, outputIndex, -1)],
{
rate: feePerKb,
maxFee,
changeAddress: receiveAddress,
}
)
fundingTransaction.sign(keyRing)

latestFundingInfo = await BitcoinHelpers.Transaction.broadcast(
fundingTransaction.toRaw().toString('hex')
)
console.log("Got transaction id", latestFundingInfo.transactionID)

latestFundingInfo.transaction =
await BitcoinHelpers.withElectrumClient(async electrumClient => {
return electrumClient.getTransaction(latestFundingInfo.transactionID)
})
console.log("Broadcast transaction", latestFundingInfo.transactionID)
})

deposit.autoSubmit()
return deposit.activeStatePromise
}

return () => {
const nextPromise = runner()
remainingPromise = remainingPromise.then(() => nextPromise)
}
}

async function doTheThing() {
web3.eth.defaultAccount = (await web3.eth.getAccounts())[0]
const tbtc = await TBTC.withConfig({
web3: web3,
bitcoinNetwork: "testnet",
electrum: {
testnet: {
server: "electrumx-server.test.tbtc.network",
port: 50002,
protocol: "ssl"
},
testnetPublic: {
server: "testnet1.bauerj.eu",
port: 50002,
protocol: "ssl"
},
testnetWS: {
server: "electrumx-server.test.tbtc.network",
port: 8443,
protocol: "wss"
}
}
})

const lotSizes = await tbtc.Deposit.availableSatoshiLotSizes()
const lowestLotSize = lotSizes.sort((a, b) => a.sub(b).toNumber())[0]
const maxFee = Math.max(
await BitcoinHelpers.withElectrumClient(async client => {
return (await client.getMinimumRelayFee()) * 10**8
}),
parseInt(await tbtc.Deposit.constantsContract.methods.getMinimumRedemptionFee().call()),
) * 2
console.log(
`Opening deposits with ${lowestLotSize.toNumber()} sats once every 2 minutes...`
)

const wdb = new WalletDB({ db: 'memory' });
await wdb.open();
wdb
const wallet = await wdb.create({
master: 'tprv8ZgxMBicQKsPfPae8Tt79fHXewcQqvEiCPyTUAPRtXYznzULBUtCYapXjcVvtWRz7fPsWUPz3bdZE3GWcbJoPifnUoKvSh8XK9g7pUdGraW',
})
const addr = await wallet.receiveAddress();
// Actually let's check its existing balance eh?

// Fund enough for 3 hours.
const fundAmount = (lowestLotSize.toNumber() + maxFee) * 90
console.log(`Please fund address with ${fundAmount} sats: ${addr.toString()}`)
console.log("Waiting...")
latestFundingInfo = await BitcoinHelpers.Transaction.findOrWaitFor(
addr.toString(),
fundAmount,
)
latestFundingInfo.transaction =
await BitcoinHelpers.Transaction.get(latestFundingInfo.transactionID)
console.log(`Found transaction ${latestFundingInfo.transactionID}, proceeding optimistically...`)

const createAndFundDeposit = fundDepositAndCreationFn(
tbtc.Deposit,
lowestLotSize,
wallet,
maxFee,
)
createAndFundDeposit()
// Then run every 2 minutes.
const interval = setInterval(
createAndFundDeposit,
2 /* minutes */ * 60 /* seconds */ * 1000 /* ms */
)

await UI.promptQuestion("Type 'quit' and enter to stop: ", ["quit"])

console.log('Stopping new deposits...')
clearInterval(interval)
console.log('Waiting for existing deposits to finish funding...')
await remainingPromise
}

doTheThing().then(a => {
console.log(a)
process.exit(0)
}).catch(e => {
console.log(e)
process.exit(1)
})
46 changes: 25 additions & 21 deletions bin/tbtc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
#!/usr/bin/env node --experimental-modules
// @ts-check
const commandHelp = `
Usage: tbtc.js <subcommand>
<subcommand> is one of:

deposit <lot-size-satoshis> [--no-mint]
Initiates a deposit funding flow. Takes the lot size in satoshis.
Will prompt with a Bitcoin address when funding needs to be
submitted.

--no-mint
specifies not to mint TBTC once the deposit is qualified.

resume <deposit-address> [--no-mint]
Resumes a deposit funding flow that did not complete. An existing
funding transaction can exist, but this can also be run before the
funding transaction is submitted.

--no-mint
specifies not to mint TBTC once the deposit is qualified.

redeem <deposit-address>
Attempts to redeem a tBTC deposit.
`
import Web3 from "web3"
import TBTC from "../index.js"

Expand Down Expand Up @@ -62,27 +86,7 @@ switch (args[0]) {
}

if (!action) {
console.log(`
Unknown command ${args[0]} or bad parameters. Supported commands:
deposit <lot-size-satoshis> [--no-mint]
Initiates a deposit funding flow. Takes the lot size in satoshis.
Will prompt with a Bitcoin address when funding needs to be
submitted.

--no-mint
specifies not to mint TBTC once the deposit is qualified.

resume <deposit-address> [--no-mint]
Resumes a deposit funding flow that did not complete. An existing
funding transaction can exist, but this can also be run before the
funding transaction is submitted.

--no-mint
specifies not to mint TBTC once the deposit is qualified.

redeem <deposit-address>
Attempts to redeem a tBTC deposit.
`)
console.log(`Unknown command ${args[0]} or bad parameters.${commandHelp}`)

process.exit(1)
}
Expand Down
41 changes: 41 additions & 0 deletions bin/ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import readline from "readline"

const prompt = readline.createInterface({
input: process.stdin,
output: process.stdout,
})

// Prompts for the question, waiting for valid answers or prefix thereof.
// Case insensitive to the entered answer, returns the selected answer from the
// valid list (not the user's input). If an invalid response is entered,
// presents an error and reprompts.
export default {
promptQuestion: async function(question, validAnswers) {
return this.promptValidInput(
question,
(found) => validAnswers.find(_ => _.toLowerCase().startsWith(found.toLowerCase())),
"Invalid answer, try one of: [" + validAnswers.join("/") + "].",
)
},

promptValidInput: async function(question, inputCheckFn, retryText) {
const answer = await this.promptInput(question)
const validAnswer = inputCheckFn(answer)

if (answer && validAnswer) {
return (typeof validAnswer == "boolean" ? answer : validAnswer)
} else {
const retry = retryText || "Invalid answer, try again.".red
console.log(retry)
return this.promptValidInput(question, inputCheckFn, retryText)
}
},

promptInput: function(question) {
return new Promise((resolve) => {
prompt.question(question + " ", (answer) => {
resolve(answer)
})
})
}
}
8 changes: 0 additions & 8 deletions jsconfig.json

This file was deleted.

Loading