mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 17:12:03 -07:00
309 lines
9.9 KiB
JavaScript
309 lines
9.9 KiB
JavaScript
/*
|
|
Adapter library for working with a wallet.
|
|
*/
|
|
|
|
// Public npm libraries
|
|
const BchWallet = require('minimal-slp-wallet/index')
|
|
|
|
// Local libraries
|
|
const JsonFiles = require('./json-files')
|
|
const config = require('../../config')
|
|
|
|
const WALLET_FILE = `${__dirname.toString()}/../../wallet.json`
|
|
// const PROOF_OF_BURN_QTY = 0.01
|
|
// const P2WDB_TOKEN_ID =
|
|
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
|
|
|
class WalletAdapter {
|
|
constructor (localConfig = {}) {
|
|
// Encapsulate dependencies
|
|
this.jsonFiles = new JsonFiles()
|
|
this.WALLET_FILE = WALLET_FILE
|
|
this.BchWallet = BchWallet
|
|
this.config = config
|
|
}
|
|
|
|
// Open the wallet file, or create one if the file doesn't exist.
|
|
// Does not instance the wallet. The output of this function is expected to
|
|
// be passed to instanceWallet().
|
|
async openWallet () {
|
|
try {
|
|
let walletData
|
|
|
|
// Try to open the wallet.json file.
|
|
try {
|
|
// console.log('this.WALLET_FILE: ', this.WALLET_FILE)
|
|
walletData = await this.jsonFiles.readJSON(this.WALLET_FILE)
|
|
} catch (err) {
|
|
// Create a new wallet file if one does not already exist.
|
|
console.warn('Wallet file not found. Creating new wallet.json file.')
|
|
|
|
// Create a new wallet.
|
|
// No-Update flag creates wallet without making any network calls.
|
|
const walletInstance = new this.BchWallet(undefined, { noUpdate: true })
|
|
|
|
// Wait for wallet to initialize.
|
|
await walletInstance.walletInfoPromise
|
|
|
|
walletData = walletInstance.walletInfo
|
|
|
|
// Add the nextAddress property
|
|
walletData.nextAddress = 1
|
|
|
|
// Write the wallet data to the JSON file.
|
|
await this.jsonFiles.writeJSON(walletData, this.WALLET_FILE)
|
|
}
|
|
|
|
// console.log('walletData: ', walletData)
|
|
|
|
return walletData
|
|
} catch (err) {
|
|
console.error('Error in openWallet()')
|
|
throw err
|
|
}
|
|
}
|
|
|
|
// Create an instance of minimal-slp-wallet. Use data in the wallet.json file,
|
|
// and pass the bch-js information to the minimal-slp-wallet library.
|
|
async instanceWallet (walletData) {
|
|
try {
|
|
// TODO: throw error if wallet data is not passed in.
|
|
|
|
const advancedConfig = {}
|
|
if (this.config.useFullStackCash) {
|
|
advancedConfig.interface = 'rest-api'
|
|
advancedConfig.restURL = this.config.apiServer
|
|
advancedConfig.apiToken = this.config.apiToken
|
|
} else {
|
|
advancedConfig.interface = 'consumer-api'
|
|
advancedConfig.restURL = this.config.consumerUrl
|
|
}
|
|
|
|
// Instantiate minimal-slp-wallet.
|
|
this.bchWallet = new this.BchWallet(walletData.mnemonic, advancedConfig)
|
|
|
|
// Wait for wallet to initialize.
|
|
await this.bchWallet.walletInfoPromise
|
|
|
|
return this.bchWallet
|
|
} catch (err) {
|
|
console.error('Error in instanceWallet()')
|
|
throw err
|
|
}
|
|
}
|
|
|
|
// Increments the 'nextAddress' property in the wallet file. This property
|
|
// indicates the HD index that should be used to generate a key pair for
|
|
// storing funds for Offers.
|
|
// This function opens the wallet file, increments the nextAddress property,
|
|
// then saves the change to the wallet file.
|
|
async incrementNextAddress () {
|
|
try {
|
|
const walletData = await this.openWallet()
|
|
// console.log('original walletdata: ', walletData)
|
|
|
|
walletData.nextAddress++
|
|
|
|
// console.log('walletData finish: ', walletData)
|
|
await this.jsonFiles.writeJSON(walletData, this.WALLET_FILE)
|
|
|
|
// Update the working instance of the wallet.
|
|
this.bchWallet.walletInfo.nextAddress++
|
|
// console.log('this.bchWallet.walletInfo: ', this.bchWallet.walletInfo)
|
|
|
|
return walletData.nextAddress
|
|
} catch (err) {
|
|
console.error('Error in incrementNextAddress()')
|
|
throw err
|
|
}
|
|
}
|
|
|
|
// This method returns an object that contains a private key WIF, public address,
|
|
// and the index of the HD wallet that the key pair was generated from.
|
|
// TODO: Allow input integer. If input is used, use that as the index. If no
|
|
// input is provided, then call incrementNextAddress().
|
|
async getKeyPair () {
|
|
try {
|
|
const hdIndex = await this.incrementNextAddress()
|
|
|
|
const mnemonic = this.bchWallet.walletInfo.mnemonic
|
|
|
|
// root seed buffer
|
|
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
|
|
|
|
const masterHDNode = this.bchWallet.bchjs.HDNode.fromSeed(rootSeed)
|
|
|
|
// HDNode of BIP44 account
|
|
// const account = this.bchWallet.bchjs.HDNode.derivePath(masterHDNode, "m/44'/245'/0'")
|
|
|
|
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
|
|
|
const cashAddress = this.bchWallet.bchjs.HDNode.toCashAddress(childNode)
|
|
console.log('Generating a new key pair for cashAddress: ', cashAddress)
|
|
|
|
const wif = this.bchWallet.bchjs.HDNode.toWIF(childNode)
|
|
|
|
const outObj = {
|
|
cashAddress,
|
|
wif,
|
|
hdIndex
|
|
}
|
|
|
|
return outObj
|
|
} catch (err) {
|
|
console.error('Error in getKeyPair()')
|
|
throw err
|
|
}
|
|
}
|
|
|
|
// Generate a cryptographic signature, required to write to the P2WDB.
|
|
async generateSignature (message) {
|
|
try {
|
|
// TODO: Add input validation for message.
|
|
|
|
const privKey = this.bchWallet.walletInfo.privateKey
|
|
|
|
// console.log('privKey: ', privKey)
|
|
// console.log('flags.data: ', flags.data)
|
|
|
|
const signature = this.bchWallet.bchjs.BitcoinCash.signMessageWithPrivKey(
|
|
privKey,
|
|
message
|
|
)
|
|
|
|
return signature
|
|
} catch (err) {
|
|
console.error('Error in generateSignature()')
|
|
throw err
|
|
}
|
|
}
|
|
|
|
// Generate a partial transcation to *take* a 'sell' order.
|
|
async generatePartialTx (orderInfo) {
|
|
try {
|
|
console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`)
|
|
|
|
const bchjs = this.bchWallet.bchjs
|
|
|
|
// instance of transaction builder
|
|
const transactionBuilder = new bchjs.TransactionBuilder()
|
|
|
|
// Get a payment UTXO.
|
|
// TODO: Create a segregated UTXO.
|
|
const bchUtxos = this.bchWallet.utxos.utxoStore.bchUtxos
|
|
const paymentUtxo = await bchjs.Utxo.findBiggestUtxo(bchUtxos)
|
|
console.log('paymentUtxo: ', paymentUtxo)
|
|
|
|
// Get token info on the offered UTXO
|
|
const txData = await this.bchWallet.getTxData([orderInfo.utxoTxid])
|
|
console.log(`txData: ${JSON.stringify(txData, null, 2)}`)
|
|
|
|
// Construct the UTXO being offered for sale.
|
|
const offeredUtxo = {
|
|
txid: orderInfo.utxoTxid,
|
|
vout: orderInfo.utxoVout,
|
|
tokenId: orderInfo.tokenId,
|
|
decimals: txData[0].tokenDecimals,
|
|
tokenQty: orderInfo.numTokens.toString()
|
|
}
|
|
console.log(`offeredUtxo: ${JSON.stringify(offeredUtxo, null, 2)}`)
|
|
|
|
// Build First part of the collaborative Tx a.k.a. Alice
|
|
// Generate the OP_RETURN code.
|
|
const slpSendObj = bchjs.SLP.TokenType1.generateSendOpReturn(
|
|
[offeredUtxo],
|
|
orderInfo.numTokens.toString()
|
|
)
|
|
const slpData = slpSendObj.script
|
|
console.log(`slpOutputs: ${slpSendObj.outputs}`)
|
|
|
|
// Currently this app only supports a single SLP token UTXO for exact
|
|
// token quantities (no token change). e.g. 1 UTXO representing the
|
|
// exact number of 'numTokens'
|
|
if (slpSendObj.outputs > 1) {
|
|
console.log('WARNING: choose one UTXO with all tokens to exchange')
|
|
return
|
|
}
|
|
|
|
// Calculate sats needed to pay the offer.
|
|
const satsNeeded = orderInfo.numTokens * parseInt(orderInfo.rateInSats)
|
|
|
|
// Calculate miner fees.
|
|
// Get byte count (minimum 2 inputs, 3 outputs)
|
|
const opReturnBufLength = slpData.byteLength + 32 // add padding
|
|
const byteCount =
|
|
bchjs.BitcoinCash.getByteCount({ P2PKH: 2 }, { P2PKH: 4 }) +
|
|
opReturnBufLength
|
|
const totalSatsNeeded = byteCount + satsNeeded
|
|
// console.log(`satoshis needed: ${satsNeeded}`)
|
|
|
|
// One last check to ensure the app wallet has enough BCH to complete
|
|
// the trade.
|
|
if (totalSatsNeeded > paymentUtxo.value) {
|
|
console.log(`Selected payment UTXO is not big enough. Sats needed: ${totalSatsNeeded}, UTXO value: ${paymentUtxo.value}`)
|
|
}
|
|
|
|
// add UTXO for sell(STILL CANNOT SPEND - not signed yet)
|
|
transactionBuilder.addInput(orderInfo.utxoTxid, orderInfo.utxoVout)
|
|
|
|
// add payment UTXO
|
|
transactionBuilder.addInput(paymentUtxo.tx_hash, paymentUtxo.tx_pos)
|
|
|
|
const originalAmount = paymentUtxo.value
|
|
const dust = 546
|
|
const remainder = originalAmount - satsNeeded - dust // exchange fee + token UTXO dust
|
|
|
|
// Add the SLP OP_RETURN data as the first output.
|
|
transactionBuilder.addOutput(slpData, 0)
|
|
|
|
const buyerAddr = this.bchWallet.walletInfo.legacyAddress
|
|
// console.log(`buyAddr: ${JSON.stringify(buyAddr, null, 2)}`)
|
|
|
|
// Send dust transaction representing tokens being sent.
|
|
transactionBuilder.addOutput(
|
|
buyerAddr,
|
|
dust
|
|
)
|
|
|
|
// Get seller address
|
|
// TODO: Seller should explicitly define the address to send to in the
|
|
// orderInfo object. Right now it retrieves the address from the UTXO
|
|
// for sale, which is not ideal.
|
|
const sellerAddr = txData[0].vout[1].scriptPubKey.addresses[0]
|
|
|
|
// Send payment to the offer side
|
|
transactionBuilder.addOutput(sellerAddr, satsNeeded)
|
|
|
|
// Send the BCH change back to the buyer
|
|
if (remainder > 550) {
|
|
transactionBuilder.addOutput(buyerAddr, remainder)
|
|
}
|
|
|
|
const buyerECPair = bchjs.ECPair.fromWIF(this.bchWallet.walletInfo.privateKey)
|
|
|
|
// Sign the buyers input UTXO for spending.
|
|
transactionBuilder.sign(
|
|
1,
|
|
buyerECPair,
|
|
null,
|
|
transactionBuilder.hashTypes.SIGHASH_ALL,
|
|
originalAmount
|
|
)
|
|
|
|
const tx = transactionBuilder.transaction.buildIncomplete()
|
|
|
|
const hex = tx.toHex()
|
|
console.log('hex: ', hex)
|
|
|
|
return hex
|
|
} catch (err) {
|
|
console.error('Error in wallet.js/generatePartialTx(): ', err)
|
|
throw err
|
|
}
|
|
|
|
// return true
|
|
}
|
|
}
|
|
|
|
module.exports = WalletAdapter
|