Skip to content

Commit

Permalink
Merge branch 'main' into ruben/ENG-267
Browse files Browse the repository at this point in the history
  • Loading branch information
topether21 committed Dec 8, 2023
2 parents 720d898 + faf29cc commit 8570ee8
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 560 deletions.
9 changes: 9 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ EXCLUDE_TAGS_HIGH_FEE_THRESHOLD=60
#MIN_TAG_SIZES_HIGH_FEE_THRESHOLD=50
#MIN_TAG_SIZES_HIGH_FEE="vintage_nakamoto:10000 block_78:10000"

# It is a configuration option that allows you to map specific tags to Bitcoin addresses.
# This is useful when you want to direct certain types of sats to specific addresses.
# The format for this configuration is an array of tag:address pairs, specified in order of priority.
# The script will use the first matching tag to determine where to send the sat.
# For example, if your configuration is uncommon:address123 omega:address345 and the script finds an uncommon omega sat, it will be sent to address123.
# However, if your configuration is omega:address345 uncommon:address123, the sat would be sent to address345.
# Therefore, the order of the tag:address pairs in your configuration matters.
#TAG_BY_ADDRESS="vintage_nakamoto:bc1p1.... block_78:bc1p2...."

# You can split up the output(s) to your exchange under certain conditions to help shake things up and maybe unlock new coins.
# Valid options are NEVER (never split), NO_SATS (only split if no sats found), or ALWAYS (always split)
#SPLIT_TRIGGER=NO_SATS
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
node_modules
data
.DS_Store
coverage/
coverage/
README.local.md
50 changes: 48 additions & 2 deletions __tests__/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { get_excluded_tags, get_min_tag_sizes, get_included_tags, satoshi_to_BTC } = require('../utils')
const { get_excluded_tags, get_min_tag_sizes, get_included_tags, get_tag_by_address, sleep, satoshi_to_BTC } = require('../utils')

describe('get_excluded_tags', () => {
test('should return correct format', () => {
Expand Down Expand Up @@ -202,4 +202,50 @@ describe('satoshi_to_BTC', () => {
const result = satoshi_to_BTC(satoshi);
expect(result).toBe(0);
});
});
});

describe('get_tag_by_address', () => {
test('should return correct format', () => {
process.env.TAG_BY_ADDRESS = 'tag1:address1 tag2:address2'
const result = get_tag_by_address()
expect(result).toEqual({ 'tag1': 'address1', 'tag2': 'address2' })
})

test('should trim leading and trailing spaces', () => {
process.env.TAG_BY_ADDRESS = ' tag1:address1 tag2:address2 '
const result = get_tag_by_address()
expect(result).toEqual({ 'tag1': 'address1', 'tag2': 'address2' })
})

test('should return null when TAG_BY_ADDRESS is not set', () => {
delete process.env.TAG_BY_ADDRESS
const result = get_tag_by_address()
expect(result).toBeNull()
})

test('should return null when TAG_BY_ADDRESS is empty', () => {
process.env.TAG_BY_ADDRESS = ' '
const result = get_tag_by_address()
expect(result).toBeNull()
})

test('should handle multiple tag-address pairs', () => {
process.env.TAG_BY_ADDRESS = 'tag1:address1 tag2:address2 tag3:address3'
const result = get_tag_by_address()
expect(result).toEqual({ 'tag1': 'address1', 'tag2': 'address2', 'tag3': 'address3' })
})
})

describe('sleep', () => {
test('should wait for the specified amount of time', async () => {
const startTime = Date.now()
await sleep(1000) // wait for 1 second
const endTime = Date.now()

// Check if the difference between the start and end times is close to 1000 milliseconds
// We use toBeGreaterThanOrEqual and toBeLessThan to account for slight variations in timing
expect(endTime - startTime).toBeGreaterThanOrEqual(1000)
expect(endTime - startTime).toBeLessThan(1010)
})
})

67 changes: 67 additions & 0 deletions __tests__/wallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const ecc = require('tiny-secp256k1')
const {BIP32Factory} = require('bip32')
const bip32 = BIP32Factory(ecc)
const bitcoin = require('bitcoinjs-lib')
const bip39 = require('bip39')

const derivePath = "m/84'/0'/0'/0/0"

// Generate a random seed
const seed = bip39.generateMnemonic()

// Derive the root key from the seed
const root = bip32.fromSeed(Buffer.from(bip39.mnemonicToSeedSync(seed)))

// Derive the first account's node (BIP84 derivation path for the first account)
const node = root.derivePath("m/84'/0'/0'/0/0")

// Get the Bitcoin p2wpkh address associated with this node
const { address } = bitcoin.payments.p2wpkh({ pubkey: node.publicKey })

process.env.LOCAL_WALLET_SEED = seed
process.env.LOCAL_WALLET_ADDRESS = address
process.env.LOCAL_DERIVATION_PATH = derivePath

const { get_utxos } = require('../wallet')
const { listunspent } = require('../bitcoin')
const axios = require('axios')

jest.mock('../bitcoin')
jest.mock('axios')

describe('get_utxos', () => {
beforeEach(() => {
// Reset the mocks before each test
listunspent.mockReset()
axios.get.mockReset()
})

test('should return correct utxos for local wallet', async () => {
delete process.env.BITCOIN_WALLET
process.env.WALLET_TYPE = 'local'
axios.get.mockImplementation(() => Promise.resolve({
data: [
{ txid: 'tx1', vout: 0, value: 100000 },
{ txid: 'tx2', vout: 1, value: 200000 }
]
}))

const result = await get_utxos()
expect(result).toEqual(['tx1:0', 'tx2:1'])
})

test('should throw error when LOCAL_WALLET_ADDRESS is not set', async () => {
delete process.env.BITCOIN_WALLET
delete process.env.LOCAL_WALLET_ADDRESS

await expect(get_utxos()).rejects.toThrow('LOCAL_WALLET_ADDRESS must be set')
})

test('should throw error when mempool api is unreachable', async () => {
delete process.env.BITCOIN_WALLET
process.env.LOCAL_WALLET_ADDRESS = 'address'
axios.get.mockImplementation(() => Promise.reject(new Error('Network error')))

await expect(get_utxos()).rejects.toThrow('Error reaching mempool api')
})
})
5 changes: 4 additions & 1 deletion deezy.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function check_api_key() {
throw new Error('DEEZY_API_KEY must be set')
}
}
async function post_scan_request({ utxo, exchange_address, rare_sat_address, extraction_fee_rate, excluded_tags = null, included_tags = null, min_tag_sizes = null }) {
async function post_scan_request({ utxo, exchange_address, rare_sat_address, extraction_fee_rate, excluded_tags = null, included_tags = null, min_tag_sizes = null, tag_by_address = null }) {
check_api_key()
if (!process.env.RARE_SAT_ADDRESS) {
throw new Error('RARE_SAT_ADDRESS must be set')
Expand All @@ -35,6 +35,9 @@ async function post_scan_request({ utxo, exchange_address, rare_sat_address, ext
if (min_tag_sizes) {
body.min_tag_sizes = min_tag_sizes
}
if (tag_by_address) {
body.tag_by_address = tag_by_address
}
if (process.env.SPLIT_TRIGGER) {
if (!VALID_SPLIT_TRIGGERS.includes(process.env.SPLIT_TRIGGER)) {
throw new Error(`Invalid SPLIT_TRIGGER: ${process.env.SPLIT_TRIGGER}, must be one of ${VALID_SPLIT_TRIGGERS.join(', ')}`)
Expand Down
9 changes: 7 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const { get_fee_rate } = require('./fees')
const { post_scan_request, get_scan_request, get_user_limits } = require('./deezy')
const { generate_satributes_messages } = require('./satributes')
const { sendNotifications, TELEGRAM_BOT_ENABLED, PUSHOVER_ENABLED } = require('./notifications.js')
const { get_excluded_tags, get_included_tags, get_min_tag_sizes, satoshi_to_BTC } = require('./utils.js')
const { get_excluded_tags, get_included_tags, get_min_tag_sizes, sleep, get_tag_by_address, satoshi_to_BTC } = require('./utils.js')

const LOOP_SECONDS = process.env.LOOP_SECONDS ? parseInt(process.env.LOOP_SECONDS) : 10
const available_exchanges = Object.keys(exchanges)
const FALLBACK_MAX_FEE_RATE = 200
Expand All @@ -25,7 +26,6 @@ let notified_bank_run = false
let notified_withdrawal_disabled = false
let notified_error_withdrawing = false

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
async function maybe_withdraw(exchange_name, exchange) {
const btc_balance = await exchange.get_btc_balance().catch((err) => {
console.error(err)
Expand Down Expand Up @@ -213,6 +213,11 @@ async function run() {
console.log(`Using min tag sizes: ${min_tag_sizes}`)
request_body.min_tag_sizes = min_tag_sizes
}
let tag_by_address = get_tag_by_address()
if (tag_by_address) {
console.log(`Using tag by address: ${Object.entries(tag_by_address).map(([tag, address]) => `${tag}:${address}`).join(' ')}`)
request_body.tag_by_address = tag_by_address
}
const scan_request = await post_scan_request(request_body)
scan_request_ids.push(scan_request.id)
if (rescanned_utxos.has(utxo)) {
Expand Down
3 changes: 2 additions & 1 deletion jest.setup.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
require("dotenv").config();
// Lets skip .env variables in our tests please set them in CI/CD if needed
// require("dotenv").config();
Loading

0 comments on commit 8570ee8

Please sign in to comment.