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

coinbase prime withdrawals implemented #25

Merged
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
7 changes: 7 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ EXCLUDE_TAGS_MEDIUM_FEE_THRESHOLD=30
EXCLUDE_TAGS_HIGH_FEE="omega alpha pizza special_name name_palindrome palindrome 3_digits/palindrome block_286 block_78 nakamoto"
EXCLUDE_TAGS_HIGH_FEE_THRESHOLD=60

# You can also include tags to hunt for alternatively. If you include tags, the script will only hunt for sats with those tags and will overwrite exclude tags.
#INCLUDE_TAGS="uncommon rare block_9"
#INCLUDE_TAGS_MEDIUM_FEE="uncommon rare"
#INCLUDE_TAGS_MEDIUM_FEE_THRESHOLD=30
#INCLUDE_TAGS_HIGH_FEE="rare"
#INCLUDE_TAGS_HIGH_FEE_THRESHOLD=60

# Set a minimum size for sat ranges (e.g. block 78 sats). This is useful if you want to only hunt for sat ranges of a minimum size.
# For example, if you indicate block_78:1000, the script will not sequester a block 78 chunk unless the size is at least 1000 sats
#MIN_TAG_SIZES="vintage_nakamoto:1000 block_78:2000"
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/test-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Test Coverage

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18

- name: Install dependencies
run: npm ci

- name: Run tests with coverage
run: npm run test -- --coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
fail_ci_if_error: true
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.env
node_modules
data
data
.DS_Store
coverage/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Automated Sat Hunter
Automated script to hunt for rare sats.

[![codecov](https://codecov.io/gh/deezy-inc/sat-hunter/graph/badge.svg?token=Z6LUE3D7FQ)](https://codecov.io/gh/deezy-inc/sat-hunter)

## Requirements
- An account on one of Coinbase, Coinbase Exchange, Kraken, Gemini, Bitfinex, Binance, Bybit (see [Exchanges section](https://github.com/deezy-inc/sat-hunter#exchanges))
- Deezy API Token (email [email protected] to request one)
Expand Down
72 changes: 71 additions & 1 deletion __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 } = require('../utils') // replace with your actual file path
const { get_excluded_tags, get_min_tag_sizes, get_included_tags } = require('../utils')

describe('get_excluded_tags', () => {
test('should return correct format', () => {
Expand Down Expand Up @@ -87,11 +87,81 @@ describe('get_min_tag_sizes', () => {
expect(result).toEqual({ 'block_9': 2000, 'block_78': 3000 })
})

test('should use medium fee min tag sizes when fee_rate is higher than MIN_TAG_SIZES_MEDIUM_FEE_THRESHOLD but lower than MIN_TAG_SIZES_HIGH_FEE_THRESHOLD', () => {
process.env.MIN_TAG_SIZES = "vintage_nakamoto:1000 block_78:2000"
process.env.MIN_TAG_SIZES_MEDIUM_FEE_THRESHOLD = 20
process.env.MIN_TAG_SIZES_MEDIUM_FEE = "vintage_nakamoto:5000 block_78:5000"
process.env.MIN_TAG_SIZES_HIGH_FEE_THRESHOLD = 50
process.env.MIN_TAG_SIZES_HIGH_FEE = "vintage_nakamoto:10000 block_78:10000"
const result = get_min_tag_sizes({ fee_rate: 30 })
expect(result).toEqual({ 'vintage_nakamoto': 5000, 'block_78': 5000 })
})

test('should not use high fee min tag sizes when fee_rate is lower than MIN_TAG_SIZES_HIGH_FEE_THRESHOLD', () => {
process.env.MIN_TAG_SIZES_HIGH_FEE_THRESHOLD = '10'
process.env.MIN_TAG_SIZES_HIGH_FEE = 'block_9:2000 block_78:3000'
process.env.MIN_TAG_SIZES = 'block_9:1000 block_78:2000'
const result = get_min_tag_sizes({ fee_rate: 5 })
expect(result).toEqual({ 'block_9': 1000, 'block_78': 2000 })
})
})

describe('get_included_tags', () => {
test('should return correct format', () => {
process.env.INCLUDE_TAGS = 'omega alpha pizza/omega omega/alpha/pizza'
const result = get_included_tags({ fee_rate: 0 })
expect(result).toEqual([['omega'], ['alpha'], ['pizza', 'omega'], ['omega', 'alpha', 'pizza']])
})

test('should trim leading and trailing spaces', () => {
process.env.INCLUDE_TAGS = ' omega alpha pizza/omega '
const result = get_included_tags({ fee_rate: 0 })
expect(result).toEqual([['omega'], ['alpha'], ['pizza', 'omega']])
})

test('should return empty array when INCLUDE_TAGS is empty string', () => {
process.env.INCLUDE_TAGS = ''
const result = get_included_tags({ fee_rate: 0 })
expect(result).toEqual([])
})

test('should return empty array when INCLUDE_TAGS is not set', () => {
delete process.env.INCLUDE_TAGS
const result = get_included_tags({ fee_rate: 0 })
expect(result).toEqual([])
})

test('should return correct format when tags contain multiple slashes', () => {
process.env.INCLUDE_TAGS = 'omega/alpha/pizza'
const result = get_included_tags({ fee_rate: 0 })
expect(result).toEqual([['omega', 'alpha', 'pizza']])
})

test('should use high fee included tags when fee_rate is higher than INCLUDE_TAGS_HIGH_FEE_THRESHOLD', () => {
process.env.INCLUDE_TAGS_HIGH_FEE_THRESHOLD = '10'
process.env.INCLUDE_TAGS_HIGH_FEE = 'alpha/pizza'
process.env.INCLUDE_TAGS_MEDIUM_FEE_THRESHOLD = '5'
process.env.INCLUDE_TAGS_MEDIUM_FEE = 'special_name'
process.env.INCLUDE_TAGS = 'omega alpha pizza/omega'
const result = get_included_tags({ fee_rate: 20 })
expect(result).toEqual([['alpha', 'pizza']])
})

test('should not use high fee included tags when fee_rate is lower than INCLUDE_TAGS_HIGH_FEE_THRESHOLD', () => {
process.env.INCLUDE_TAGS_HIGH_FEE_THRESHOLD = '10'
process.env.INCLUDE_TAGS_HIGH_FEE = 'omega/pizza'
process.env.INCLUDE_TAGS = 'omega alpha pizza/omega'
const result = get_included_tags({ fee_rate: 5 })
expect(result).toEqual([['omega'], ['alpha'], ['pizza', 'omega']])
})

test('should use medium fee included tags when fee_rate is in the middle', () => {
process.env.INCLUDE_TAGS_HIGH_FEE_THRESHOLD = '20'
process.env.INCLUDE_TAGS_HIGH_FEE = 'omega/pizza'
process.env.INCLUDE_TAGS_MEDIUM_FEE_THRESHOLD = '10'
process.env.INCLUDE_TAGS_MEDIUM_FEE = 'special_name'
process.env.INCLUDE_TAGS = 'omega alpha pizza/omega'
const result = get_included_tags({ fee_rate: 15 })
expect(result).toEqual([['special_name']])
})
})
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, 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 }) {
check_api_key()
if (!process.env.RARE_SAT_ADDRESS) {
throw new Error('RARE_SAT_ADDRESS must be set')
Expand All @@ -29,6 +29,9 @@ async function post_scan_request({ utxo, exchange_address, rare_sat_address, ext
if (excluded_tags) {
body.excluded_tags = excluded_tags
}
if (included_tags) {
body.included_tags = included_tags
}
if (min_tag_sizes) {
body.min_tag_sizes = min_tag_sizes
}
Expand Down
41 changes: 27 additions & 14 deletions exchanges/coinbase-prime.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
const axios = require('axios')
const crypto = require('crypto')
const totp = require("totp-generator")
const BASE_URL = 'https://api.prime.coinbase.com/'
const CryptoJS = require('crypto-js');
const uuid = require('uuid');

const BASE_URL = 'https://api.prime.coinbase.com'

let BTC_WALLET_ID = null
let PORTFOLIO_ID = null
let ENTITY_ID = null

function create_signature({ path, timestamp, method, body = ''}) {
const data = `${timestamp}${method}${path}${body}`
const base64_key = Buffer.from(process.env.COINBASE_PRIME_SIGNING_KEY, 'base64')
return crypto.createHmac('sha256', base64_key)
.update(data)
.digest('base64');
function sign(str, secret) {
const hash = CryptoJS.HmacSHA256(str, secret);
return hash.toString(CryptoJS.enc.Base64);
}
function buildPayload(ts, method, requestPath, body) {
return `${ts}${method}${requestPath}${body}`;
}

function create_headers({ path, timestamp, method, body = '' }) {
const strToSign = buildPayload(timestamp, method, path, body);
const sig = sign(strToSign, process.env.COINBASE_PRIME_SIGNING_KEY);

return {
'Content-Type': 'application/json',
'X-CB-ACCESS-KEY': process.env.COINBASE_PRIME_ACCESS_KEY,
'X-CB-ACCESS-PASSPHRASE': process.env.COINBASE_PRIME_API_PASSPHRASE,
'X-CB-ACCESS-TIMESTAMP': timestamp,
'X-CB-ACCESS-SIGNATURE': create_signature({ path, timestamp, method, body })
'X-CB-ACCESS-SIGNATURE': sig
}
}

Expand All @@ -38,7 +46,7 @@ async function set_coinbase_ids() {
}

async function set_btc_wallet_id() {
const path = `/v1/portfolios/${PORTFOLIO_ID}/wallets?type=TRADING&symbol=BTC`
const path = `/v1/portfolios/${PORTFOLIO_ID}/wallets` // ?type=TRADING&symbol=BTC
const timestamp = `${Math.floor(Date.now() / 1000)}`
const method = 'GET'
const headers = create_headers({ path, timestamp, method })
Expand All @@ -64,7 +72,7 @@ async function check_and_set_credentials() {

async function get_btc_balance() {
await check_and_set_credentials()
const path = `/v1/portfolios/${PORTFOLIO_ID}/wallets/${BTC_WALLET_ID}`
const path = `/v1/portfolios/${PORTFOLIO_ID}/wallets/${BTC_WALLET_ID}/balance`
const timestamp = `${Math.floor(Date.now() / 1000)}`
const method = 'GET'
const headers = create_headers({ path, timestamp, method })
Expand All @@ -83,10 +91,15 @@ async function withdraw({ amount_btc }) {
const method = 'POST'
const body = {
amount: amount_btc.toFixed(8),
currency: 'BTC',
profile_id: BTC_ACCOUNT_ID,
crypto_address: process.env.COINBASE_PRIME_WITHDRAWAL_ADDRESS,
add_network_fee_to_total: false,
currency_symbol: 'BTC',
portfolio_id: PORTFOLIO_ID,
wallet_id: BTC_WALLET_ID,
idempotency_key: uuid.v4(),
destination_type: 'DESTINATION_BLOCKCHAIN',
blockchain_address: {
address: process.env.COINBASE_PRIME_WITHDRAWAL_ADDRESS,
account_identifier: 'trezor-sat-hunter'
}
}
if (process.env.COINBASE_PRIME_TOTP_SECRET) {
body.two_factor_code = totp(process.env.COINBASE_PRIME_TOTP_SECRET)
Expand Down
2 changes: 1 addition & 1 deletion exchanges/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ const kucoin = require('./kucoin')

module.exports = {
bitfinex, kraken, coinbase, gemini, binance, coinbase_exchange, coinbase_prime, okx, bybit, kucoin
}
}
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const { get_fee_rate } = require('./fees')
const { post_scan_request, get_scan_request } = require('./deezy')
const { generate_satributes_messages } = require('./satributes')
const { sendNotifications, TELEGRAM_BOT_ENABLED, PUSHOVER_ENABLED } = require('./notifications.js')
const { get_excluded_tags, get_min_tag_sizes } = require('./utils.js')
const { get_excluded_tags, get_included_tags, get_min_tag_sizes } = 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 Down Expand Up @@ -204,6 +204,11 @@ async function run() {
console.log(`Using excluded tags: ${excluded_tags}`)
request_body.excluded_tags = excluded_tags
}
let included_tags = get_included_tags({ fee_rate })
if (included_tags) {
console.log(`Using included tags: ${included_tags}`)
request_body.included_tags = included_tags
}
let min_tag_sizes = get_min_tag_sizes({ fee_rate })
if (min_tag_sizes) {
console.log(`Using min tag sizes: ${min_tag_sizes}`)
Expand Down
Loading
Loading