Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verbose notifications #8

Merged
merged 2 commits into from
Mar 23, 2024
Merged
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
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ OKCOIN_DEPOSIT_WALLET=


# Notifications
# change to verbose to get all notifications
NOTIFICATION_LEVEL="info"

SLACK_WEB_HOOK=

TELEGRAM_BOT_TOKEN=
Expand Down
3 changes: 3 additions & 0 deletions conf/satminer.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ if (!INCLUDE_SATRIBUTES) {
console.log('only these satributes will be extracted!');
}

const { NOTIFICATION_LEVEL = 'info' } = process.env;

// The wallets will be loaded from ENV by priority from top to bottom
const RARE_SAT_KNOWN_TYPES = [
'uncommon',
Expand Down Expand Up @@ -194,4 +196,5 @@ module.exports = {
INCLUDE_SATRIBUTES,
MIN_OUTPUT_SIZE,
CUSTOM_SPECIAL_SAT_WALLETS,
NOTIFICATION_LEVEL,
};
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const {
CUSTOM_SPECIAL_SAT_WALLETS,
ORDINALSBOT_API_KEY,
INCLUDE_SATRIBUTES,
NOTIFICATION_LEVEL,
} = require('./conf/satminer');
const NotificationService = require('./model/notifications/notificationService');
const SlackNotifications = require('./model/notifications/slack');
Expand All @@ -51,7 +52,7 @@ const satextractor = new Satextractor(ORDINALSBOT_API_KEY, "live");
const mempool = new Mempool(ORDINALSBOT_API_KEY, "live");

// initialize notifications
let notifications = new NotificationService();
let notifications = new NotificationService(NOTIFICATION_LEVEL);
if (SLACK_WEB_HOOK) {
console.log('enabling slack webhook notifications');
const slackWebHook = new SlackNotifications(SLACK_WEB_HOOK);
Expand Down
6 changes: 4 additions & 2 deletions model/notifications/notificationService.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
class NotificationService {
constructor() {
constructor(notificationLevel = 'info') {
this.notifiers = [];
this.notificationLevel = notificationLevel;
}

addNotifier(notifier) {
this.notifiers.push(notifier);
}

sendMessage(message) {
sendMessage(message, severity = 'info') {
if (this.notificationLevel === 'info' && severity === 'verbose') return;
this.notifiers.forEach(notifier => {
notifier.sendMessage(message);
});
Expand Down
5 changes: 5 additions & 0 deletions model/satminer.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ class Satminer {
const fees = await this.wallet.estimateFee();
const { fastestFee } = fees;
if (fastestFee > this.maxFeeAllowed) {
this.notificationService.sendMessage(`stopped: fee higher than max allowed ${this.maxFeeAllowed}`, 'verbose');
throw new Error(`fee higher than max allowed ${this.maxFeeAllowed}`);
}

Expand All @@ -152,6 +153,7 @@ class Satminer {
console.log('tumbler wallet is empty');
return false;
} else if (specialRanges.length === 0) {
this.notificationService.sendMessage('no special sats found, sending funds back to exchange', 'verbose');
console.log('no special sats found, sending funds back to exchange');
await this.checkLocalBalance();
}
Expand All @@ -162,18 +164,21 @@ class Satminer {
const outputsNok = decodedTransaction.vout.find((output) => !this.userControlledAddresses.includes(output.scriptPubKey.address));
console.log('outputsNok', outputsNok, this.userControlledAddresses);
if (outputsNok) {
this.notificationService.sendMessage('stopped: not all outputs are user-controlled', 'verbose');
throw new Error('not all outputs are user-controlled');
}

// check that what we are depositing to exchange is more than minDepositAmount
const exchangeOutput = decodedTransaction.vout.find((output) => output.scriptPubKey.address === this.addressCommonSats);
if (exchangeOutput.scriptPubKey.value < this.minDepositAmount) {
this.notificationService.sendMessage('stopped: deposit amount is less than minDepositAmount', 'verbose');
throw new Error('deposit amount is less than minDepositAmount');
}

const signedTx = await this.wallet.signRawTransaction(tx);
const txid = await this.wallet.sendRawTransaction(signedTx);
console.log('sent txid', txid);
this.notificationService.sendMessage(`end of cycle. sent txid: ${txid}`, 'verbose');

if (specialRanges.length > 0) {
const rangeSummary = this.specialRangesSummary(specialRanges);
Expand Down
72 changes: 0 additions & 72 deletions model/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,53 +80,6 @@ class Wallet {
confTarget,
);

/**
* create a raw bitcoin transaction manually,
* instead of using bitcoind createrawtransaction.
* This is needed because bitcoind does not support
* having duplicate output addresses.
* @param {Input[]} inputs - Array of inputs
* @param {Output[]} outputs - Input object
* @param {string[]} inputsRawTx - Array of raw transactions
* @returns {string} Raw transaction hex
*
* // Inputs and outputs arrays
* const inputs = [
* { hash: 'transactionId1', index: 0 },
* { hash: 'transactionId2', index: 1 }
* ];
* const outputs = [
* { address: address1, value: amount1 },
* { address: address2, value: amount2 }
* ];
*/
createRawTransaction = (inputs, outputs, inputsRawTx = null) => {
// Without this sending to taproot addresses is failing
bitcoinjs.initEccLib(ecc);

const { network } = this;
const txb = new bitcoinjs.Psbt({ network });

inputs.forEach((input, i) => {
const x = {
...input,
sequence: bitcoinjs.Transaction.DEFAULT_SEQUENCE - 2, // Enable RBF
};

// Add the raw transaction if it exists
if (inputsRawTx) {
x.nonWitnessUtxo = Buffer.from(inputsRawTx[i], 'hex');
}

txb.addInput(x);
});
outputs.forEach((output) => txb.addOutput({ ...output }));

// Build the transaction
const tx = txb.data.globalMap.unsignedTx.toBuffer().toString('hex');
return tx;
};

/**
* @param {Input[]} inputs - the inputs
* @param {Output[]} outputs - the outputs
Expand Down Expand Up @@ -161,31 +114,6 @@ class Wallet {

sendRawTransaction = async (signedTx) => this.bitcoinClient.sendRawTransaction(signedTx);

/**
*
* @typedef {object} Input
* @property {string} hash - Transaction hash
* @property {number} index - Transaction index
* @typedef {object} Output
* @property {string} address - Address
* @property {number} value - Value in satoshis
* @param {Input[]} inputs - Array of inputs
* @param {Output[]} outputs - Input object
* @returns {Promise<string>} txId - Transaction id
*/
sendCustomTransaction = async (inputs, outputs) => {
// Create raw transaction
const rawTx = await this.createUnsignedHexTransaction(inputs, outputs);

// Sign raw transaction
const signedTx = await this.signRawTransaction(rawTx);

// Send raw transaction
const txId = await this.sendRawTransaction(signedTx);

return txId;
};

decodeRawTransaction = async (rawTx) => this.bitcoinClient.decodeRawTransaction(rawTx);
}

Expand Down
27 changes: 25 additions & 2 deletions test/satminer.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ describe('Satminer', () => {
const sendTxSpy = sinon.stub(wallet, 'sendRawTransaction').resolves(txid);
const decodeRawTxSpy = sinon.stub(wallet, 'decodeRawTransaction').resolves({ vout: [{ scriptPubKey: { address: addressReceiveCommonSats, value: 0.1 } }] });
const signRawTxSpy = sinon.stub(wallet, 'signRawTransaction').resolves('signedtx');
const notificationSpy = sinon.stub(notifications, 'sendMessage').resolves(true);

const walletBalance = 1;
const confirmationTargetBlocks = 2;
Expand All @@ -113,7 +114,7 @@ describe('Satminer', () => {
addressReceiveCommonSats,
confirmationTargetBlocks,
null,
null,
notifications,
['uncommon'],
);
const res = await satminer.extractSatsAndRotateFunds();
Expand All @@ -133,6 +134,7 @@ describe('Satminer', () => {
assert(signRawTxSpy.calledWith(mockSatExtractorApiResponse.tx));
assert(sendTxSpy.calledOnce);
assert(sendTxSpy.calledWith('signedtx'));
assert(notificationSpy.calledTwice);
});

it('should send common sats to exchange wallet', async () => {
Expand Down Expand Up @@ -166,6 +168,7 @@ describe('Satminer', () => {
const sendTxSpy = sinon.stub(mockWallet, 'sendRawTransaction').resolves(txid);
const decodeRawTxSpy = sinon.stub(mockWallet, 'decodeRawTransaction').resolves({ vout: [{ scriptPubKey: { address: krakenDepoAddr, value: 0.1 } }] });
const signRawTxSpy = sinon.stub(mockWallet, 'signRawTransaction').resolves('signedtx');
const notificationSpy = sinon.stub(notifications, 'sendMessage').resolves(true);

satminer = new Satminer(
mockWallet,
Expand All @@ -176,6 +179,7 @@ describe('Satminer', () => {
krakenDepoAddr,
confirmationTargetBlocks,
minDepositAmount,
notifications,
);

const res = await satminer.extractSatsAndRotateFunds();
Expand All @@ -193,6 +197,7 @@ describe('Satminer', () => {
assert(signRawTxSpy.calledWith(mockSatExtractorApiResponse.tx));
assert(sendTxSpy.calledOnce);
assert(sendTxSpy.calledWith('signedtx'));
assert(notificationSpy.calledTwice);
});

it('should throw if common sats sent to exchange wallet if below min deposit amount', async () => {
Expand Down Expand Up @@ -226,6 +231,7 @@ describe('Satminer', () => {
const sendTxSpy = sinon.stub(mockWallet, 'sendRawTransaction').resolves(txid);
const decodeRawTxSpy = sinon.stub(mockWallet, 'decodeRawTransaction').resolves({ vout: [{ scriptPubKey: { address: krakenDepoAddr, value: 0.004 } }] });
const signRawTxSpy = sinon.stub(mockWallet, 'signRawTransaction').resolves('signedtx');
const notificationSpy = sinon.stub(notifications, 'sendMessage').resolves(true);

satminer = new Satminer(
mockWallet,
Expand All @@ -236,6 +242,7 @@ describe('Satminer', () => {
krakenDepoAddr,
confirmationTargetBlocks,
minDepositAmount,
notifications,
);

await assert.rejects(async () => satminer.extractSatsAndRotateFunds(), { message: 'deposit amount is less than minDepositAmount' });
Expand All @@ -253,6 +260,7 @@ describe('Satminer', () => {
assert(signRawTxSpy.notCalled);
assert(sendTxSpy.notCalled);
assert(sendTxSpy.notCalled);
assert(notificationSpy.calledTwice);
});

it('should throw if any funds go to non-user controlled addresses', async () => {
Expand Down Expand Up @@ -287,6 +295,7 @@ describe('Satminer', () => {
const sendTxSpy = sinon.stub(mockWallet, 'sendRawTransaction').resolves(txid);
const decodeRawTxSpy = sinon.stub(mockWallet, 'decodeRawTransaction').resolves({ vout: [{ scriptPubKey: { address: randomAddress, value: 0.01 }}, { scriptPubKey: { address: krakenDepoAddr, value: 0.02 }} ] });
const signRawTxSpy = sinon.stub(mockWallet, 'signRawTransaction').resolves('signedtx');
const notificationSpy = sinon.stub(notifications, 'sendMessage').resolves(true);

satminer = new Satminer(
mockWallet,
Expand All @@ -297,6 +306,7 @@ describe('Satminer', () => {
krakenDepoAddr,
confirmationTargetBlocks,
minDepositAmount,
notifications,
);

await assert.rejects(async () => satminer.extractSatsAndRotateFunds(), { message: 'not all outputs are user-controlled' });
Expand All @@ -314,6 +324,7 @@ describe('Satminer', () => {
assert(signRawTxSpy.notCalled);
assert(sendTxSpy.notCalled);
assert(sendTxSpy.notCalled);
assert(notificationSpy.calledTwice);
});

it('should finish quietly when address is empty', async () => {
Expand Down Expand Up @@ -366,6 +377,7 @@ describe('Satminer', () => {

const mockMempoolApi = new MempoolApi();
sinon.stub(mockMempoolApi, 'getFeeEstimation').resolves(mockFeeEst);
const notificationSpy = sinon.stub(notifications, 'sendMessage').resolves(true);

const scanner = new Satscanner();

Expand All @@ -374,9 +386,20 @@ describe('Satminer', () => {
const inventoryWallet = 'inventorywalletaddr';
const krakenDepoAddr = 'krakendepoaddr';
const mockWallet = new Wallet(null, mockMempoolApi);
const satminer = new Satminer(mockWallet, scanner, satextractor, tumblerAddress, inventoryWallet, krakenDepoAddr, null, minDepositAmount);
const satminer = new Satminer(
mockWallet,
scanner,
satextractor,
tumblerAddress,
inventoryWallet,
krakenDepoAddr,
null,
minDepositAmount,
notifications,
);

await assert.rejects(async () => satminer.extractSatsAndRotateFunds(), { message: 'fee higher than max allowed 1000' });
assert(notificationSpy.calledOnce);
});
});

Expand Down
Loading
Loading