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

[WIP] feat: cardano support #1278

Open
wants to merge 7 commits into
base: future-stuff-old-dev
Choose a base branch
from
Open
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: 2 additions & 1 deletion lib/new-admin/config/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const _ = require('lodash/fp')

const { ALL } = require('../../plugins/common/ccxt')

const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR } = COINS
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, ADA } = COINS
const { bitpay, coinbase, itbit, bitstamp, kraken, binanceus, cex, ftx, binance } = ALL

const TICKER = 'ticker'
Expand Down Expand Up @@ -37,6 +37,7 @@ const ALL_ACCOUNTS = [
{ code: 'monerod', display: 'monerod', class: WALLET, cryptos: [XMR] },
{ code: 'bitcoincashd', display: 'bitcoincashd', class: WALLET, cryptos: [BCH] },
{ code: 'bitgo', display: 'BitGo', class: WALLET, cryptos: [BTC, ZEC, LTC, BCH, DASH] },
{ code: 'blockfrost', display: 'Blockfrost', class: WALLET, cryptos: [ADA] },
{ code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: bitstamp.CRYPTO },
{ code: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: itbit.CRYPTO },
{ code: 'kraken', display: 'Kraken', class: EXCHANGE, cryptos: kraken.CRYPTO },
Expand Down
3 changes: 2 additions & 1 deletion lib/new-settings-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const SECRET_FIELDS = [
'ftx.privateKey',
'cex.privateKey',
'binance.privateKey',
'twilio.authToken'
'twilio.authToken',
'blockfrost.projectId'
]

/*
Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/common/ccxt.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const bitpay = require('../ticker/bitpay')
const binance = require('../exchange/binance')
const logger = require('../../logger')

const { BTC, BCH, DASH, ETH, LTC, ZEC } = COINS
const { BTC, BCH, DASH, ETH, LTC, ZEC, ADA } = COINS

const ALL = {
cex: cex,
Expand All @@ -22,7 +22,7 @@ const ALL = {
itbit: itbit,
bitpay: bitpay,
coinbase: {
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH],
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, ADA],
FIAT: 'ALL_CURRENCIES'
},
binance: binance
Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/exchange/binance.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const { ORDER_TYPES } = require('./consts')

const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, XMR, ETH, LTC, ZEC } = COINS
const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR]
const { BTC, BCH, XMR, ETH, LTC, ZEC, ADA } = COINS
const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, ADA]
const FIAT = ['USD', 'EUR']
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']

Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/exchange/binanceus.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const { ORDER_TYPES } = require('./consts')

const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, ZEC } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH]
const { BTC, BCH, DASH, ETH, LTC, ZEC, ADA } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, ADA]
const FIAT = ['USD']
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']

Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/exchange/bitstamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const { ORDER_TYPES } = require('./consts')

const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, ETH, LTC, BCH } = COINS
const CRYPTO = [BTC, ETH, LTC, BCH]
const { BTC, ETH, LTC, BCH, ADA } = COINS
const CRYPTO = [BTC, ETH, LTC, BCH, ADA]
const FIAT = ['USD', 'EUR']
const AMOUNT_PRECISION = 8
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId']
Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/exchange/cex.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const { ORDER_TYPES } = require('./consts')

const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, BCH]
const { BTC, BCH, DASH, ETH, LTC, ADA } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, ADA]
const FIAT = ['USD', 'EUR']
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']

Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/exchange/kraken.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
const { COINS } = require('@lamassu/coins')

const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR]
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, ADA } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, ADA]
const FIAT = ['USD', 'EUR']
const AMOUNT_PRECISION = 6
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
Expand Down
127 changes: 127 additions & 0 deletions lib/plugins/wallet/blockfrost/blockfrost.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const _ = require('lodash/fp')
const { utils: coinUtils } = require('@lamassu/coins')
const BN = require('../../../bn')
const Blockfrost = require('@blockfrost/blockfrost-js')
const { toEntropyBuffer, fromSeed } = require('../../../mnemonic-helpers')
const CardanoWasm = require('@emurgo/cardano-serialization-lib-nodejs')
const helper = require('../helper')

const NAME = 'Blockfrost'
const SUPPORTED_COINS = ['ADA']
const UNIT = 'lovelace'
const GAP_LIMIT = 20
const EXTERNAL_CHAIN = 0
const INTERNAL_CHAIN = 1

const mnemonic = helper.readMnemonic()
const walletHash = helper.computeWalletHash(mnemonic)

/**
*
TODO: Requires testing and possibly refactoring after Electrum wallet changes
*
*/

function buildBlockfrost (account) {
return new Blockfrost.BlockFrostAPI({
projectId: account.projectId
})
}

function getRootKey (account) {
const masterSeed = account.seed
if (!masterSeed) throw new Error('No master seed!')
const entropy = toEntropyBuffer(fromSeed(masterSeed))
return CardanoWasm.Bip32PrivateKey.from_bip39_entropy(
entropy,
Buffer.from('')
)
}

function generateKeys (rootKey, index, type) {
const accountKey = rootKey
.derive(0x80000000 + 1852) // purpose
.derive(0x80000000 + 1815) // coin type
.derive(0x80000000 + 0) // account #0

const utxoPubKey = accountKey
.derive(type) // external = 0 or internal = 1
.derive(index) // address_index
.to_public()

const stakeKey = accountKey
.derive(2) // chimeric
.derive(0) // account #0
.to_public()

return { utxoPubKey, stakeKey }
}

function getWalletAddresses (account, cryptoCode) {
const rootKey = getRootKey(account)
helper.freeAddressIndeces(cryptoCode, walletHash, ['internal', 'external'])
.then(({ internal, external }) => {
// wallets addresses with staking key
const addresses = []
for (let i = 0; i <= external + GAP_LIMIT; i++) {
addresses.push(getBaseAddress(rootKey, external, EXTERNAL_CHAIN))
}
for (let i = 0; i <= internal + GAP_LIMIT; i++) {
addresses.push(getBaseAddress(rootKey, internal, INTERNAL_CHAIN))
}
return addresses
})
}

function getBaseAddress (rootKey, index, type) {
const { utxoPubKey, stakeKey } = generateKeys(rootKey, index, type)
return CardanoWasm.BaseAddress.new(
CardanoWasm.NetworkInfo.testnet().network_id(),
CardanoWasm.StakeCredential.from_keyhash(utxoPubKey.to_raw_key().hash()),
CardanoWasm.StakeCredential.from_keyhash(stakeKey.to_raw_key().hash())
).to_address().to_bech32()
}

function fetchBalance (account, addresses) {
const API = buildBlockfrost(account)
const promises = _.map(address => API.addresses(address))(addresses)
return Promise.all(promises)
.then(addressesInfo => {
const amounts = _.map(({ amount }) => new BN(_.find(result => result.unit === UNIT)(amount)).quantity)(addressesInfo)
return _.sum(amounts)
})
}

function balance (account, cryptoCode, settings, operatorId) {
return checkCryptoCode(cryptoCode)
.then(() => getWalletAddresses(account, cryptoCode))
.then(addresses => fetchBalance(account, addresses))
}

function newAddress (account, info, tx, settings, operatorId) {
return getWalletAddress(account)
}

function checkBlockchainStatus (cryptoCode, account) {
return checkCryptoCode(cryptoCode)
.then(() => buildBlockfrost(account).health())
.then(isHealthy => isHealthy ? 'ready' : 'syncing')
}

function checkCryptoCode (cryptoCode) {
if (!SUPPORTED_COINS.includes(cryptoCode)) {
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
}

return Promise.resolve()
}

module.exports = {
NAME,
balance,
// sendCoins,
newAddress,
// getStatus,
// newFunding,
checkBlockchainStatus
}
127 changes: 127 additions & 0 deletions lib/plugins/wallet/cardano-wallet/cardano-wallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const _ = require('lodash/fp')
const { utils: coinUtils } = require('@lamassu/coins')
const BN = require('../../../bn')

const { WalletServer, AddressWallet } = require('cardano-wallet-js')
const SUPPORTED_COINS = ['ADA']

const cryptoRec = coinUtils.getCryptoCurrency('ADA')
const unitScale = cryptoRec.unitScale

function buildWalletServer (url) {
return WalletServer.init(url)
}

// getWallet(account).then(w => w.getUsedAddresses()).then(console.log)
// newAddress(account, { cryptoCode: 'ADA' }).then(addr => console.log(addr))
// balance(account, 'ADA').then(b => console.log(b))
// getStatus(account, { toAddress: 'addr_test1qpfxxyax9wac0tft6jrmpzcdq286hna47g9eyklycggh6ej0jh9y5x8nspe86h7vesmkmlzz2xg6ascmhe9xfga4valqdp08vv', cryptoCode: 'ADA' }, new BN(1000000000)).then(console.log)
// sendCoins(account, { toAddress: 'addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3', cryptoAtoms: new BN(2000000), cryptoCode: 'ADA' }).then(console.log)

function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
const { toAddress, cryptoAtoms, cryptoCode } = tx
const receiverAddress = [new AddressWallet(toAddress)]
const amounts = [cryptoAtoms.toString()]

return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => wallet.sendPayment(account.passphrase, receiverAddress, amounts))
}

function getStatus (account, tx, requested, settings, operatorId) {
const { toAddress, cryptoCode } = tx
return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => wallet.getTransactions())
.then(txs => {
// Assumes that the our receiver address was only used once
const tx = _.head(_.filter(tx =>
tx.direction === 'incoming' &&
_.includes(toAddress, _.map(output => output.address)(tx.outputs))
)(txs))
if (tx) {
const amount = new BN(_.find(output => output.address === toAddress)(tx.outputs).amount.quantity)
// Should we care about rollbacks? As per my understanding 'in_ledger' state can be reverted to 'submitted'
// and if ttl expires it can go to 'expired' state
if (tx.status === 'in_ledger' && amount.gte(requested)) return { receivedCryptoAtoms: amount, status: 'confirmed' }
}
return { receivedCryptoAtoms: 0, status: 'notSeen' }
})
}

/**
* TODO: we should manage addresses internally, the problem lies in the possibility of consecutive cash-out
* transactions with the same address, since the list of unused wallet addresses is only updated upon a on-chain tx
*/
function newAddress (account, info, tx, settings, operatorId) {
return checkCryptoCode(info.cryptoCode)
.then(() => getWallet(account))
.then(wallet => wallet.getUnusedAddresses())
.then(addresses => addresses.slice(0, 1))
}

// function getWalletPendingBalance (wallet) {
// return wallet.getTransactions()
// .then(txs => {
// const getPendingTxs = (direction, txs) => _.filter(tx => !_.includes(tx.status, ['in_ledger']) && tx.direction === direction)(txs)
// const sumBalance = _.sumBy(tx => tx.amount)
// const incomingPendingTxs = getPendingTxs('incoming', txs)
// const outgoingPendingTxs = getPendingTxs('outgoing', txs)
// return sumBalance(incomingPendingTxs) - sumBalance(outgoingPendingTxs)
// })
// }

function newFunding (account, cryptoCode, settings, operatorId) {
return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => {
const promises = [
newAddress(account),
new BN(0), // is the pending balance insignificant or should we use something like getWalletPendingBalance?
wallet.getAvailableBalance()
]

return Promise.all(promises)
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
})
}

function balance (account, cryptoCode, settings, operatorId) {
return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => wallet.getAvailableBalance())
.then(balance => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
}

function getWallet (account) {
const walletServer = buildWalletServer(account.url)
return walletServer.wallets()
.then(wallets => walletServer.getShelleyWallet(_.find(wallet => wallet.id === account.walletId, wallets).id))
}

function checkBlockchainStatus (cryptoCode, account) {
return checkCryptoCode(cryptoCode)
.then(() => buildWalletServer(account.url).getNetworkInformation())
.then(networkInfo => networkInfo.sync_progress.status)
}

function checkCryptoCode (cryptoCode) {
if (!SUPPORTED_COINS.includes(cryptoCode)) {
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
}

return Promise.resolve()
}

module.exports = {
balance,
sendCoins,
newAddress,
getStatus,
newFunding,
checkBlockchainStatus
}
2 changes: 1 addition & 1 deletion lib/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ function checkBlockchainStatus (settings, cryptoCode) {
}

return Promise.resolve(require(walletDaemons[cryptoCode]))
.then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode))
.then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode, settings))
}

const coinFilter = ['ETH']
Expand Down
25 changes: 25 additions & 0 deletions new-lamassu-admin/src/pages/Services/schemas/blockfrost.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as Yup from 'yup'

import SecretInputFormik from 'src/components/inputs/formik/SecretInput'

import { secretTest } from './helper'

export default {
code: 'blockfrost',
name: 'Blockfrost',
title: 'Blockfrost (Wallet)',
elements: [
{
code: 'projectId',
display: 'Project ID',
component: SecretInputFormik
}
],
getValidationSchema: account => {
return Yup.object().shape({
projectId: Yup.string('The project id must be a string')
.max(100, 'The project id is too long')
.test(secretTest(account?.projectId, 'project id'))
})
}
}
Loading