From 0011779eec0ab3ee2aa4e3b6b024484368629e78 Mon Sep 17 00:00:00 2001 From: ordinariusprof Date: Thu, 21 Mar 2024 22:27:11 -0700 Subject: [PATCH 1/3] add coinbase api --- api/coinbase.js | 90 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 api/coinbase.js diff --git a/api/coinbase.js b/api/coinbase.js new file mode 100644 index 0000000..98b7cb5 --- /dev/null +++ b/api/coinbase.js @@ -0,0 +1,90 @@ +const axios = require('axios'); +const crypto = require('crypto'); + +class CoinbaseAPI { + constructor(apiKey, apiSecret, apiPassphrase) { + this.apiKey = apiKey; + this.apiSecret = apiSecret; + this.apiPassphrase = apiPassphrase; + this.baseURL = 'https://api.coinbase.com'; + } + + async getPublicEndpoint(endpoint) { + try { + const response = await axios.get(`${this.baseURL}${endpoint}`); + return response.data; + } catch (error) { + console.error('Error:', error.message); + throw error; + } + } + + async getAuthEndpoint(endpoint, body) { + try { + const headers = await this.signMessage('GET', endpoint, body); + const response = await axios.get(`${this.baseURL}${endpoint}`, { headers }); + return response.data; + } catch (error) { + console.error('Error:', error.message); + throw error; + } + } + + async postAuthEndpoint(endpoint, body) { + try { + const headers = await this.signMessage('POST', endpoint, body); + const response = await axios.post(`${this.baseURL}${endpoint}`, body, { headers }); + return response.data; + } catch (error) { + console.error('Error:', error.message); + throw error; + } + } + + async signMessage(method, endpoint, body) { + const cb_access_timestamp = Date.now() / 1000; // in ms + + // create the prehash string by concatenating required parts + const message = `${cb_access_timestamp}${method}${endpoint}${JSON.stringify(body)}`; + + // decode the base64 secret + const key = Buffer.from(this.apiSecret, 'base64'); + + // create a sha256 hmac with the secret + const hmac = crypto.createHmac('sha256', key); + + // sign the required message with the hmac and base64 encode the result + const cb_access_sign = hmac.update(message).digest('base64'); + + return { + 'CB-ACCESS-KEY': this.apiKey, + 'CB-ACCESS-SIGN': cb_access_sign, + 'CB-ACCESS-TIMESTAMP': cb_access_timestamp, + 'CB-ACCESS-PASSPHRASE': this.apiPassphrase, + 'Content-Type': 'application/json', + }; + } + + async getSystemStatus() { + return this.getPublicEndpoint('/system/status'); + } + + async getAccountBalance() { + return this.getAuthEndpoint('/accounts'); + } + + async withdrawFunds(amount, currency, address) { + const body = { + amount, + currency, + crypto_address: address, + }; + return this.postAuthEndpoint('/withdrawals/crypto', body); + } + + async getServerTime() { + return this.getPublicEndpoint('/time'); + } +} + +module.exports = CoinbaseAPI; From 1a350e489d45766d0dec1467519f8b8d734c0a77 Mon Sep 17 00:00:00 2001 From: ordinariusprof Date: Tue, 26 Mar 2024 15:39:43 -0700 Subject: [PATCH 2/3] wip --- .env.sample | 6 ++++ api/coinbase.js | 40 ++++++++++------------ conf/satminer.js | 29 ++++++++++++++++ index.js | 22 ++++++++++-- model/exchanges/coinbase.js | 68 +++++++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 26 deletions(-) create mode 100644 model/exchanges/coinbase.js diff --git a/.env.sample b/.env.sample index 3fcf1fe..bdd793d 100644 --- a/.env.sample +++ b/.env.sample @@ -59,6 +59,12 @@ OKCOIN_API_PASSPHRASE= OKCOIN_WITHDRAWAL_WALLET= OKCOIN_DEPOSIT_WALLET= +# coinbase +COINBASE_API_KEY= +COINBASE_API_SECRET= +# for coinbase, tag is not used for withdrawal, provide the trusted withdrawal address here +COINBASE_WITHDRAWAL_WALLET= +COINBASE_DEPOSIT_WALLET= # Notifications SLACK_WEB_HOOK= diff --git a/api/coinbase.js b/api/coinbase.js index 98b7cb5..918cb82 100644 --- a/api/coinbase.js +++ b/api/coinbase.js @@ -2,10 +2,9 @@ const axios = require('axios'); const crypto = require('crypto'); class CoinbaseAPI { - constructor(apiKey, apiSecret, apiPassphrase) { + constructor(apiKey, apiSecret) { this.apiKey = apiKey; this.apiSecret = apiSecret; - this.apiPassphrase = apiPassphrase; this.baseURL = 'https://api.coinbase.com'; } @@ -42,48 +41,43 @@ class CoinbaseAPI { } async signMessage(method, endpoint, body) { - const cb_access_timestamp = Date.now() / 1000; // in ms - - // create the prehash string by concatenating required parts - const message = `${cb_access_timestamp}${method}${endpoint}${JSON.stringify(body)}`; - - // decode the base64 secret - const key = Buffer.from(this.apiSecret, 'base64'); - - // create a sha256 hmac with the secret - const hmac = crypto.createHmac('sha256', key); - - // sign the required message with the hmac and base64 encode the result - const cb_access_sign = hmac.update(message).digest('base64'); - + const timestamp = Math.floor(Date.now() / 1000); // Unix time in seconds + const message = `${timestamp}${method}${endpoint}${JSON.stringify(body)}`; + const signature = crypto.createHmac('sha256', this.apiSecret).update(message).digest('hex'); return { 'CB-ACCESS-KEY': this.apiKey, - 'CB-ACCESS-SIGN': cb_access_sign, - 'CB-ACCESS-TIMESTAMP': cb_access_timestamp, - 'CB-ACCESS-PASSPHRASE': this.apiPassphrase, + 'CB-ACCESS-SIGN': signature, + 'CB-ACCESS-TIMESTAMP': timestamp, 'Content-Type': 'application/json', }; } async getSystemStatus() { - return this.getPublicEndpoint('/system/status'); + throw new Error('Not implemented'); + } + + async getAccountId() { + const accounts = await this.getAuthEndpoint('/v2/accounts/BTC'); + return accounts.data.data.id; } async getAccountBalance() { - return this.getAuthEndpoint('/accounts'); + const accountId = await this.getAccountId(); + return this.getAuthEndpoint(`/v2/accounts/${accountId}`); } async withdrawFunds(amount, currency, address) { + const accountId = await this.getAccountId(); const body = { amount, currency, crypto_address: address, }; - return this.postAuthEndpoint('/withdrawals/crypto', body); + return this.postAuthEndpoint(`/v2/accounts/${accountId}/transactions`, body); } async getServerTime() { - return this.getPublicEndpoint('/time'); + return this.getPublicEndpoint('/v2/time'); } } diff --git a/conf/satminer.js b/conf/satminer.js index 5084e5f..ddab90d 100644 --- a/conf/satminer.js +++ b/conf/satminer.js @@ -28,6 +28,7 @@ console.log('MIN_DEPOSIT_AMOUNT', MIN_DEPOSIT_AMOUNT); let EXCHANGE_DATA = { ACTIVE_EXCHANGE: process.env.ACTIVE_EXCHANGE, }; +let EXCHANGE_DEPOSIT_WALLET; if (process.env.ACTIVE_EXCHANGE === 'kraken') { const { KRAKEN_API_KEY, KRAKEN_API_SECRET } = process.env; if (!KRAKEN_API_KEY || !KRAKEN_API_SECRET) { @@ -51,6 +52,7 @@ if (process.env.ACTIVE_EXCHANGE === 'kraken') { KRAKEN_WITHDRAW_CURRENCY, KRAKEN_DEPOSIT_WALLET, }; + EXCHANGE_DEPOSIT_WALLET = KRAKEN_DEPOSIT_WALLET; } else if (process.env.ACTIVE_EXCHANGE === 'okcoin') { const { OKCOIN_API_KEY, OKCOIN_API_SECRET, OKCOIN_API_PASSPHRASE } = process.env; if (!OKCOIN_API_KEY || !OKCOIN_API_SECRET || !OKCOIN_API_PASSPHRASE) { @@ -75,6 +77,32 @@ if (process.env.ACTIVE_EXCHANGE === 'kraken') { OKCOIN_WITHDRAW_CURRENCY, OKCOIN_DEPOSIT_WALLET, }; + EXCHANGE_DEPOSIT_WALLET = OKCOIN_DEPOSIT_WALLET; +} else if (process.env.ACTIVE_EXCHANGE === 'coinbase') { + const { COINBASE_API_KEY, COINBASE_API_SECRET } = process.env; + if (!COINBASE_API_KEY || !COINBASE_API_SECRET) { + throw Error('Missing COINBASE_API_KEY or COINBASE_API_SECRET environment variable'); + } + const { COINBASE_WITHDRAWAL_WALLET } = process.env; + if (!COINBASE_WITHDRAWAL_WALLET) { + throw Error('Missing COINBASE_WITHDRAWAL_WALLET environment variable'); + } + console.log('COINBASE_WITHDRAWAL_WALLET', COINBASE_WITHDRAWAL_WALLET); + const COINBASE_WITHDRAW_CURRENCY = 'BTC'; + const { COINBASE_DEPOSIT_WALLET } = process.env; + if (!COINBASE_DEPOSIT_WALLET) { + throw new Error('Missing COINBASE_DEPOSIT_WALLET environment variable'); + } + EXCHANGE_DATA = { + ...EXCHANGE_DATA, + COINBASE_API_KEY, + COINBASE_API_SECRET, + COINBASE_WITHDRAWAL_WALLET, + COINBASE_WITHDRAW_CURRENCY, + COINBASE_DEPOSIT_WALLET, + }; + EXCHANGE_DEPOSIT_WALLET = COINBASE_DEPOSIT_WALLET; + } else { throw new Error('Unknown exchange'); } @@ -170,6 +198,7 @@ console.log('CUSTOM_SPECIAL_SAT_WALLETS', CUSTOM_SPECIAL_SAT_WALLETS); module.exports = { ORDINALSBOT_API_KEY, EXCHANGE_DATA, + EXCHANGE_DEPOSIT_WALLET, MAX_WITHDRAWAL_AMOUNT, MIN_WITHDRAWAL_AMOUNT, MIN_DEPOSIT_AMOUNT, diff --git a/index.js b/index.js index b162b50..44dff39 100644 --- a/index.js +++ b/index.js @@ -4,13 +4,16 @@ const { Satscanner, Satextractor, Mempool } = require('ordinalsbot'); const Satminer = require('./model/satminer'); const KrakenAPI = require('./api/kraken'); const OkcoinAPI = require('./api/okcoin'); +const CoinbaseAPI = require('./api/coinbase'); const MempoolApi = require('./api/mempool'); const KrakenTumbler = require('./model/exchanges/kraken'); const OkcoinTumbler = require('./model/exchanges/okcoin'); +const CoinbaseTumbler = require('./model/exchanges/coinbase'); const Wallet = require('./model/wallet'); const { loadBitcoinWallet } = require('./utils/funcs'); const { EXCHANGE_DATA, + EXCHANGE_DEPOSIT_WALLET, MAX_WITHDRAWAL_AMOUNT, MIN_WITHDRAWAL_AMOUNT, MIN_DEPOSIT_AMOUNT, @@ -58,13 +61,16 @@ const { KRAKEN_API_SECRET, KRAKEN_WITHDRAWAL_WALLET, KRAKEN_WITHDRAW_CURRENCY, - KRAKEN_DEPOSIT_WALLET, + EXCHANGE_DEPOSIT_WALLET, OKCOIN_API_KEY, OKCOIN_API_SECRET, OKCOIN_API_PASSPHRASE, OKCOIN_WITHDRAWAL_WALLET, OKCOIN_WITHDRAW_CURRENCY, - OKCOIN_DEPOSIT_WALLET, + COINBASE_API_KEY, + COINBASE_API_SECRET, + COINBASE_WITHDRAWAL_WALLET, + COINBASE_WITHDRAW_CURRENCY, } = EXCHANGE_DATA; const sweepConfirmationTargetBlocks = 1; @@ -74,7 +80,7 @@ const satminer = new Satminer( satextractor, TUMBLER_ADDRESS, INVENTORY_WALLET, - ACTIVE_EXCHANGE === 'kraken' ? KRAKEN_DEPOSIT_WALLET : OKCOIN_DEPOSIT_WALLET, + EXCHANGE_DEPOSIT_WALLET, sweepConfirmationTargetBlocks, MIN_DEPOSIT_AMOUNT, slackWebHook, @@ -104,6 +110,16 @@ switch (ACTIVE_EXCHANGE) { OKCOIN_WITHDRAW_CURRENCY, ); break; + case 'coinbase': + const coinbaseAPI = new CoinbaseAPI(COINBASE_API_KEY, COINBASE_API_SECRET); + exchangeTumbler = new CoinbaseTumbler( + coinbaseAPI, + MIN_WITHDRAWAL_AMOUNT, + MAX_WITHDRAWAL_AMOUNT, + COINBASE_WITHDRAWAL_WALLET, + COINBASE_WITHDRAW_CURRENCY, + ); + break; default: throw new Error(`Unknown exchange ${ACTIVE_EXCHANGE}`); } diff --git a/model/exchanges/coinbase.js b/model/exchanges/coinbase.js new file mode 100644 index 0000000..e6f612e --- /dev/null +++ b/model/exchanges/coinbase.js @@ -0,0 +1,68 @@ +const CoinbaseAPI = require('../../api/coinbase'); + +/** + * Rotates funds from a Coinbase account + */ +class CoinbaseTumbler { + /** + * @param {CoinbaseAPI} coinbaseClient + * @param {number} minWithdrawalAmount + * @param {number} maxWithdrawalAmount + * @param {string} withdrawWallet + * @param {string} withdrawCurrency + */ + constructor( + coinbaseClient, + minWithdrawalAmount, + maxWithdrawalAmount, + withdrawWallet, + withdrawCurrency, + ) { + this.coinbaseClient = coinbaseClient; + this.minWithdrawalAmount = minWithdrawalAmount; + this.maxWithdrawalAmount = maxWithdrawalAmount; + this.withdrawWallet = withdrawWallet; + this.withdrawCurrency = withdrawCurrency; + } + + withdrawAvailableFunds = async () => { + const btcBalance = balance.data.find((b) => b.ccy === 'BTC').availBal; + if (btcBalance < this.minWithdrawalAmount) { + console.log(`insufficient funds to withdraw, account balance ${btcBalance}`); + return false; + } + + let withdrawalAmount = Number(btcBalance).toFixed(8);; + if (btcBalance > this.maxWithdrawalAmount) { + withdrawalAmount = this.maxWithdrawalAmount; + } + + const withdrawalFees = await this.coinbaseClient.getWithdrawalFee(this.withdrawCurrency); + let btcFee = withdrawalFees.data.find((f) => f.chain === 'BTC-Bitcoin').maxFee; + btcFee = parseFloat(btcFee); + withdrawalAmount -= btcFee; + withdrawalAmount = Number(withdrawalAmount).toFixed(8); + + console.log( + `withdrawing ${withdrawalAmount} ${this.withdrawCurrency} to wallet ${this.withdrawWallet}`, + ); + + const res = await this.coinbaseClient.withdrawFunds( + this.withdrawCurrency, + this.withdrawWallet, + withdrawalAmount, + btcFee, + ); + console.log('response from coinbase', res); + + if (res.msg) { + console.error('error calling coinbase api', res.msg); + return false; + } + + console.log('successful withdrawal from coinbase'); + return true; + }; +} + +module.exports = CoinbaseTumbler; From da49973cddfc3620a21a5dba694c3634849ed539 Mon Sep 17 00:00:00 2001 From: ordinariusprof Date: Thu, 28 Mar 2024 06:33:31 +0100 Subject: [PATCH 3/3] coinbase ready --- api/coinbase.js | 28 ++++++++++++++++++---------- index.js | 1 - model/exchanges/coinbase.js | 19 ++++++++----------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/api/coinbase.js b/api/coinbase.js index 918cb82..1c0e525 100644 --- a/api/coinbase.js +++ b/api/coinbase.js @@ -6,6 +6,7 @@ class CoinbaseAPI { this.apiKey = apiKey; this.apiSecret = apiSecret; this.baseURL = 'https://api.coinbase.com'; + this.accountId = null; } async getPublicEndpoint(endpoint) { @@ -31,7 +32,7 @@ class CoinbaseAPI { async postAuthEndpoint(endpoint, body) { try { - const headers = await this.signMessage('POST', endpoint, body); + const headers = await this.signMessage('POST', endpoint, JSON.stringify(body)); const response = await axios.post(`${this.baseURL}${endpoint}`, body, { headers }); return response.data; } catch (error) { @@ -40,14 +41,15 @@ class CoinbaseAPI { } } - async signMessage(method, endpoint, body) { + async signMessage(method, endpoint, body = '') { const timestamp = Math.floor(Date.now() / 1000); // Unix time in seconds - const message = `${timestamp}${method}${endpoint}${JSON.stringify(body)}`; + const message = `${timestamp}${method}${endpoint}${body}`; const signature = crypto.createHmac('sha256', this.apiSecret).update(message).digest('hex'); return { 'CB-ACCESS-KEY': this.apiKey, 'CB-ACCESS-SIGN': signature, - 'CB-ACCESS-TIMESTAMP': timestamp, + 'CB-ACCESS-TIMESTAMP': `${timestamp}`, + 'CB-VERSION': '2024-03-22', 'Content-Type': 'application/json', }; } @@ -58,22 +60,28 @@ class CoinbaseAPI { async getAccountId() { const accounts = await this.getAuthEndpoint('/v2/accounts/BTC'); - return accounts.data.data.id; + return accounts.data.id; } async getAccountBalance() { - const accountId = await this.getAccountId(); - return this.getAuthEndpoint(`/v2/accounts/${accountId}`); + if (!this.accountId) { + this.accountId = await this.getAccountId(); + } + return this.getAuthEndpoint(`/v2/accounts/${this.accountId}`); } async withdrawFunds(amount, currency, address) { - const accountId = await this.getAccountId(); + if (!this.accountId) { + this.accountId = await this.getAccountId(); + } const body = { + type: 'send', amount, currency, - crypto_address: address, + to: address, + to_financial_institution: false, }; - return this.postAuthEndpoint(`/v2/accounts/${accountId}/transactions`, body); + return this.postAuthEndpoint(`/v2/accounts/${this.accountId}/transactions`, body); } async getServerTime() { diff --git a/index.js b/index.js index 44dff39..4fd3e04 100644 --- a/index.js +++ b/index.js @@ -61,7 +61,6 @@ const { KRAKEN_API_SECRET, KRAKEN_WITHDRAWAL_WALLET, KRAKEN_WITHDRAW_CURRENCY, - EXCHANGE_DEPOSIT_WALLET, OKCOIN_API_KEY, OKCOIN_API_SECRET, OKCOIN_API_PASSPHRASE, diff --git a/model/exchanges/coinbase.js b/model/exchanges/coinbase.js index e6f612e..0af8a2c 100644 --- a/model/exchanges/coinbase.js +++ b/model/exchanges/coinbase.js @@ -26,7 +26,9 @@ class CoinbaseTumbler { } withdrawAvailableFunds = async () => { - const btcBalance = balance.data.find((b) => b.ccy === 'BTC').availBal; + const balance = await this.coinbaseClient.getAccountBalance(); + + const btcBalance = balance.data.balance.amount; if (btcBalance < this.minWithdrawalAmount) { console.log(`insufficient funds to withdraw, account balance ${btcBalance}`); return false; @@ -36,11 +38,8 @@ class CoinbaseTumbler { if (btcBalance > this.maxWithdrawalAmount) { withdrawalAmount = this.maxWithdrawalAmount; } - - const withdrawalFees = await this.coinbaseClient.getWithdrawalFee(this.withdrawCurrency); - let btcFee = withdrawalFees.data.find((f) => f.chain === 'BTC-Bitcoin').maxFee; - btcFee = parseFloat(btcFee); - withdrawalAmount -= btcFee; + // deduct some random fee + withdrawalAmount -= 0.001; withdrawalAmount = Number(withdrawalAmount).toFixed(8); console.log( @@ -48,15 +47,13 @@ class CoinbaseTumbler { ); const res = await this.coinbaseClient.withdrawFunds( + `${withdrawalAmount}`, this.withdrawCurrency, this.withdrawWallet, - withdrawalAmount, - btcFee, ); - console.log('response from coinbase', res); - if (res.msg) { - console.error('error calling coinbase api', res.msg); + if (!res.data?.id) { + console.error('error calling coinbase api', res); return false; }