Skip to content

Commit

Permalink
feat: send all funds of wallet (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Apr 18, 2019
1 parent f8d1ab5 commit 63bcf46
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 28 deletions.
2 changes: 1 addition & 1 deletion lib/cli/BuilderComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default {
},
timeoutBlockNumber: {
describe: 'after how my blocks the onchain script of the swap should time out',
default: '10',
type: 'number',
default: '10',
},
};
7 changes: 6 additions & 1 deletion lib/cli/commands/SendCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BuilderComponents from '../BuilderComponents';
import { callback, loadBoltzClient } from '../Command';
import { SendCoinsRequest } from '../../proto/boltzrpc_pb';

export const command = 'sendcoins <currency> <address> <amount> [fee_per_byte]';
export const command = 'sendcoins <currency> <address> <amount> [fee_per_byte] [send_all]';

export const describe = 'sends coins to a specified address';

Expand All @@ -18,6 +18,10 @@ export const builder = {
type: 'number',
},
fee_per_byte: BuilderComponents.feePerByte,
send_all: {
describe: 'ignores the amount and sends the whole balance of the wallet',
type: 'boolean',
},
};

export const handler = (argv: Arguments<any>) => {
Expand All @@ -27,6 +31,7 @@ export const handler = (argv: Arguments<any>) => {
request.setAddress(argv.address);
request.setAmount(argv.amount);
request.setSatPerVbyte(argv.fee_per_byte);
request.setSendAll(argv.send_all);

loadBoltzClient(argv).sendCoins(request, callback);
};
6 changes: 3 additions & 3 deletions lib/db/models/Output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ class Output extends Model {

public static load = (sequelize: Sequelize) => {
Output.init({
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
id: { type: new DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
script: { type: new DataTypes.STRING(255), allowNull: false },
redeemScript: { type: new DataTypes.STRING(255), allowNull: true },
currency: { type: new DataTypes.STRING(255), allowNull: false },
keyIndex: { type: DataTypes.INTEGER, allowNull: false },
type: { type: DataTypes.BOOLEAN, allowNull: false },
keyIndex: { type: new DataTypes.INTEGER(), allowNull: false },
type: { type: new DataTypes.INTEGER(), allowNull: false },
}, {
sequelize,
timestamps: false,
Expand Down
8 changes: 4 additions & 4 deletions lib/db/models/Utxo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ class Utxo extends Model {

public static load = (sequelize: Sequelize) => {
Utxo.init({
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
outputId: { type: DataTypes.INTEGER, allowNull: false },
id: { type: new DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
outputId: { type: new DataTypes.INTEGER(), allowNull: false },
currency: { type: new DataTypes.STRING(255), allowNull: false },
txHash: { type: new DataTypes.STRING(255), allowNull: false },
vout: { type: DataTypes.INTEGER, allowNull: false },
value: { type: DataTypes.INTEGER, allowNull: false },
vout: { type: new DataTypes.INTEGER(), allowNull: false },
value: { type: new DataTypes.INTEGER(), allowNull: false },
confirmed: { type: DataTypes.BOOLEAN, allowNull: false },
spent: { type: DataTypes.BOOLEAN, allowNull: false },
}, {
Expand Down
8 changes: 4 additions & 4 deletions lib/db/models/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ class Wallet extends Model {

public static load = (sequelize: Sequelize) => {
Wallet.init({
symbol: { type: DataTypes.STRING(255), primaryKey: true, allowNull: false },
highestUsedIndex: { type: DataTypes.INTEGER, allowNull: false },
derivationPath: { type: DataTypes.STRING(255), allowNull: false },
blockHeight: { type: DataTypes.INTEGER, allowNull: false },
symbol: { type: new DataTypes.STRING(255), primaryKey: true, allowNull: false },
highestUsedIndex: { type: new DataTypes.INTEGER(), allowNull: false },
derivationPath: { type: new DataTypes.STRING(255), allowNull: false },
blockHeight: { type: new DataTypes.INTEGER(), allowNull: false },
}, {
sequelize,
timestamps: false,
Expand Down
4 changes: 4 additions & 0 deletions lib/proto/boltzrpc_pb.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 30 additions & 1 deletion lib/proto/boltzrpc_pb.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const argChecks = {
if (!(rate > 0)) throw Errors.INVALID_ARGUMENT('rate must a positive number');
},
VALID_AMOUNT: ({ amount }: { amount: number }) => {
if (!(amount > 0) || amount % 1 !== 0) throw Errors.INVALID_ARGUMENT('amount must a positive integer');
if (!(amount >= 0) || amount % 1 !== 0) throw Errors.INVALID_ARGUMENT('amount must a positive integer');
},
VALID_FEE_PER_VBYTE: ({ satPerVbyte }: { satPerVbyte: number }) => {
if (!(satPerVbyte > 0) || satPerVbyte % 1 !== 0) throw Errors.INVALID_ARGUMENT('sat per vbyte fee must be positive integer');
Expand Down Expand Up @@ -368,7 +368,7 @@ class Service extends EventEmitter {
/**
* Sends coins to a specified address
*/
public sendCoins = async (args: { currency: string, address: string, amount: number, satPerVbyte: number }) => {
public sendCoins = async (args: { currency: string, address: string, amount: number, satPerVbyte: number, sendAll: boolean }) => {
argChecks.HAS_ADDRESS(args);
argChecks.VALID_CURRENCY(args);
argChecks.VALID_AMOUNT(args);
Expand All @@ -381,7 +381,7 @@ class Service extends EventEmitter {

const output = SwapUtils.getOutputScriptType(address.toOutputScript(args.address, currency.network))!;

const { transaction, vout } = await wallet.sendToAddress(args.address, output.type, output.isSh!, args.amount, fee);
const { transaction, vout } = await wallet.sendToAddress(args.address, output.type, output.isSh!, args.amount, fee, args.sendAll);
await currency.chainClient.sendRawTransaction(transaction.toHex());

return {
Expand Down
38 changes: 28 additions & 10 deletions lib/wallet/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,24 +265,38 @@ class Wallet {
*
* @returns the transaction itself and the vout of the provided address
*/
public sendToAddress = async (address: string, type: OutputType, isScriptHash: boolean, amount: number, satsPerByte?: number):
Promise<{ transaction: Transaction, vout: number }> => {
public sendToAddress = async (
address: string,
type: OutputType,
isScriptHash: boolean,
amount: number,
satsPerByte?: number,
sendAll?: boolean): Promise<{ transaction: Transaction, vout: number }> => {

return this.lock.acquire(this.sendToAddressLock, async () => {
const utxos = await this.utxoRepository.getUtxosSorted(this.symbol);

// The UTXOs that will be spent
const toSpend: UTXO[] = [];

const feePerByte = satsPerByte || await this.chainClient.estimateFee();

const outputs: any[] = [{ type, isSh: isScriptHash }];

// Add a change address to the estimation if not all coins will be sent
if (!sendAll) {
outputs.push({ type: OutputType.Bech32 });
}

const recalculateFee = () => {
return estimateFee(feePerByte, toSpend, [{ type: OutputType.Bech32 }, { type, isSh: isScriptHash }]);
return estimateFee(feePerByte, toSpend, outputs);
};

let toSpendSum = 0;
let fee = recalculateFee();

const fundsSufficient = () => {
return (amount + fee) <= toSpendSum;
return sendAll ? false : (amount + fee) <= toSpendSum;
};

// Accumulate UTXOs to spend
Expand Down Expand Up @@ -312,7 +326,7 @@ class Wallet {
}

// Throw an error if the wallet doesn't have enough funds
if (!fundsSufficient()) {
if (!fundsSufficient() && !sendAll) {
throw Errors.NOT_ENOUGH_FUNDS(amount);
}

Expand All @@ -333,12 +347,16 @@ class Wallet {
}
});

// Add the requested ouput to the transaction
builder.addOutput(address, amount);
if (!sendAll) {
// Add the requested ouput to the transaction
builder.addOutput(address, amount);

// Send the value left of the UTXOs to a new change address
const changeAddress = await this.getNewAddress(OutputType.Bech32);
builder.addOutput(changeAddress, toSpendSum - (amount + fee));
// Send the value left of the UTXOs to a new change address
const changeAddress = await this.getNewAddress(OutputType.Bech32);
builder.addOutput(changeAddress, toSpendSum - (amount + fee));
} else {
builder.addOutput(address, toSpendSum - fee);
}

// Sign the transaction
toSpend.forEach((utxo, index) => {
Expand Down
1 change: 1 addition & 0 deletions proto/boltzrpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ message SendCoinsRequest {
string address = 2;
uint64 amount = 3;
uint32 sat_per_vbyte = 4;
bool send_all = 5;
}
message SendCoinsResponse {
string transaction_hash = 1;
Expand Down
35 changes: 34 additions & 1 deletion test/integration/wallet/Wallet.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { expect } from 'chai';
import { fromSeed } from 'bip32';
import { generateMnemonic, mnemonicToSeedSync } from 'bip39';
import { TxOutput } from 'bitcoinjs-lib';
import { OutputType, Networks } from 'boltz-core';
import { generateMnemonic, mnemonicToSeedSync } from 'bip39';
import Logger from '../../../lib/Logger';
import Wallet from '../../../lib/wallet/Wallet';
import Database from '../../../lib/db/Database';
Expand Down Expand Up @@ -118,8 +119,40 @@ describe('Wallet', () => {
expect(newBalance.totalBalance).to.be.lessThan(totalBalance);
});

it('should spend all coins', async () => {
const address = await wallet.getNewAddress(OutputType.Bech32);
await bitcoinClient.sendToAddress(address, 100000000);
await bitcoinClient.sendToAddress(address, 100000000);

await bitcoinClient.generate(1);

await waitForPromiseToBeTrue(async () => {
const balance = await wallet.getBalance();

return balance.unconfirmedBalance === 0;
});

const { totalBalance } = await wallet.getBalance();

const { transaction } = await wallet.sendToAddress(address, OutputType.Bech32, false, 0, 2, true);
const out = transaction.outs[0] as TxOutput;

expect(transaction.outs.length).to.be.equal(1);
expect(out.value).to.be.equal(totalBalance - 490);
});

it('should prefer spending confirmed coins', async () => {
const address = await wallet.getNewAddress(OutputType.Bech32);

await bitcoinClient.sendToAddress(address, 100000000);
await bitcoinClient.generate(1);

await waitForPromiseToBeTrue(async () => {
const balance = await wallet.getBalance();

return balance.unconfirmedBalance === 0;
});

const unconfirmedTransactionId = await bitcoinClient.sendToAddress(address, 100000000);
const unconfirmedTransactionHash = reverseBuffer(
getHexBuffer(
Expand Down

0 comments on commit 63bcf46

Please sign in to comment.