diff --git a/app/background/node/client.js b/app/background/node/client.js
index e77f9abd1..c7e1fdbf2 100644
--- a/app/background/node/client.js
+++ b/app/background/node/client.js
@@ -14,5 +14,7 @@ export const clientStub = ipcRendererInjector => makeClient(ipcRendererInjector,
'broadcastRawTx',
'sendRawAirdrop',
'getFees',
- 'getAverageBlockTime'
+ 'getAverageBlockTime',
+ 'finalizeWithPayment',
+ 'claimPaidTransfer'
]);
diff --git a/app/background/node/service.js b/app/background/node/service.js
index 523da0547..abeab20a8 100644
--- a/app/background/node/service.js
+++ b/app/background/node/service.js
@@ -211,6 +211,40 @@ export class NodeService extends EventEmitter {
return Math.floor((sum / count) * 1000);
}
+ async finalizeWithPayment(name, fundingAddr, nameReceiveAddr, price) {
+ this._ensureStarted();
+ const ret = new Promise((resolve, reject) => {
+ this.hsdWindow.webContents.once('ipc-message', (_, channel, reply) => {
+ if (channel !== 'finalize-with-payment-reply') {
+ return;
+ }
+ if (reply.error) {
+ return reject(reply.error);
+ }
+ resolve(reply);
+ });
+ });
+ this.hsdWindow.webContents.send('finalize-with-payment', name, fundingAddr, nameReceiveAddr, price);
+ return ret;
+ }
+
+ async claimPaidTransfer(txHex) {
+ this._ensureStarted();
+ const ret = new Promise((resolve, reject) => {
+ this.hsdWindow.webContents.once('ipc-message', (_, channel, reply) => {
+ if (channel !== 'claim-paid-transfer-reply') {
+ return;
+ }
+ if (reply.error) {
+ return reject(reply.error);
+ }
+ resolve(reply);
+ });
+ });
+ this.hsdWindow.webContents.send('claim-paid-transfer', txHex);
+ return ret;
+ }
+
_ensureStarted() {
if (!this.hsdWindow) {
throw new Error('hsd not started.');
@@ -275,6 +309,8 @@ const methods = {
sendRawAirdrop: (data) => service.sendRawAirdrop(data),
getFees: () => service.getFees(),
getAverageBlockTime: () => service.getAverageBlockTime(),
+ finalizeWithPayment: (name, fundingAddr, nameReceiveAddr, price) => service.finalizeWithPayment(name, fundingAddr, nameReceiveAddr, price),
+ claimPaidTransfer: (txHex) => service.claimPaidTransfer(txHex),
};
export async function start(server) {
diff --git a/app/hsdMain.js b/app/hsdMain.js
index 8c5bca25d..cd2f5658d 100644
--- a/app/hsdMain.js
+++ b/app/hsdMain.js
@@ -3,7 +3,10 @@ require('./sentry');
const ipc = require('electron').ipcRenderer;
const FullNode = require('hsd/lib/node/fullnode');
const WalletPlugin = require('hsd/lib/wallet/plugin');
+const Script = require('hsd/lib/script/script');
const remote = require('electron').remote;
+const {hashName, types} = require('hsd/lib/covenants/rules');
+const {Output, MTX, Address} = require('hsd/lib/primitives');
let hsd = null;
ipc.on('start', (_, prefix, net, apiKey) => {
@@ -45,7 +48,7 @@ ipc.on('start', (_, prefix, net, apiKey) => {
.then(() => hsd.startSync())
.catch((e) => {
console.log(e);
- ipc.send('error', e)
+ ipc.send('error', e);
});
});
@@ -57,3 +60,122 @@ ipc.on('close', () => {
hsd.close()
.then(() => remote.getCurrentWindow().close());
});
+
+ipc.on('finalize-with-payment', (event, name, fundingAddr, nameReceiveAddr, price) => {
+ (async () => {
+ const {wdb} = hsd.require('walletdb');
+ const wallet = await wdb.get('allison');
+ const ns = await wallet.getNameStateByName(name);
+ const owner = ns.owner;
+ const coin = await wallet.getCoin(owner.hash, owner.index);
+ const nameHash = hashName(name);
+
+ const output0 = new Output();
+ output0.value = coin.value;
+ output0.address = new Address().fromString(nameReceiveAddr);
+ output0.covenant.type = types.FINALIZE;
+ output0.covenant.pushHash(nameHash);
+ output0.covenant.pushU32(ns.height);
+ output0.covenant.push(Buffer.from(name, 'ascii'));
+ output0.covenant.pushU8(0); // flags, may be required if name was CLAIMed
+ output0.covenant.pushU32(ns.claimed);
+ output0.covenant.pushU32(ns.renewals);
+ output0.covenant.pushHash(await wdb.getRenewalBlock());
+
+ const output1 = new Output();
+ output1.address = new Address().fromString(fundingAddr);
+ output1.value = price;
+
+ const mtx = new MTX();
+ mtx.addCoin(coin);
+ mtx.outputs.push(output0);
+ mtx.outputs.push(output1);
+
+ // Sign
+ const rings = await wallet.deriveInputs(mtx);
+ assert(rings.length === 1);
+ const signed = await mtx.sign(
+ rings,
+ Script.hashType.SINGLEREVERSE | Script.hashType.ANYONECANPAY,
+ );
+ assert(signed === 1);
+
+ assert(mtx.verify());
+ return mtx.encode().toString('hex');
+ })().then((hex) => {
+ ipc.send('finalize-with-payment-reply', hex);
+ }).catch((e) => {
+ ipc.send('finalize-with-payment-reply', {
+ error: e.message,
+ });
+ });
+});
+
+ipc.on('claim-paid-transfer', (event, txHex) => {
+ (async () => {
+ const {wdb} = hsd.require('walletdb');
+ const wallet = await wdb.get('allison');
+ const mtx = MTX.decode(Buffer.from(txHex, 'hex'));
+
+ // Bob should verify all the data in the MTX to ensure everything is valid,
+ // but this is the minimum.
+ const input0 = mtx.input(0).clone(); // copy input with Alice's signature
+ const coinEntry = await hsd.chain.db.readCoin(input0.prevout);
+ assert(coinEntry); // ensures that coin exists and is still unspent
+
+ const coin = coinEntry.toCoin(input0.prevout);
+ assert(coin.covenant.type === types.TRANSFER);
+
+ // Fund the TX.
+ // The hsd wallet is not designed to handle partially-signed TXs
+ // or coins from outside the wallet, so a little hacking is needed.
+ const changeAddress = await wallet.changeAddress();
+ const rate = await wdb.estimateFee();
+ const coins = await wallet.getSmartCoins();
+ // Add the external coin to the coin selector so we don't fail assertions
+ coins.push(coin);
+ await mtx.fund(coins, {changeAddress, rate});
+ // The funding mechanism starts by wiping out existing inputs
+ // which for us includes Alice's signature. Replace it from our backup.
+ mtx.inputs[0].inject(input0);
+
+ // Rearrange outputs.
+ // Since we added a change output, the SINGELREVERSE is now broken:
+ //
+ // input 0: TRANSFER UTXO --> output 0: FINALIZE covenant
+ // input 1: Bob's funds --- output 1: payment to Alice
+ // (null) --- output 2: change to Bob
+ const outputs = mtx.outputs.slice();
+ mtx.outputs = [outputs[0], outputs[2], outputs[1]];
+
+ // Prepare to wait for mempool acceptance (race condition)
+ const waiter = new Promise((resolve, reject) => {
+ hsd.mempool.once('tx', resolve);
+ });
+
+ // Sign & Broadcast
+ // Bob uses SIGHASHALL. The final TX looks like this:
+ //
+ // input 0: TRANSFER UTXO --> output 0: FINALIZE covenant
+ // input 1: Bob's funds --- output 1: change to Bob
+ // (null) --- output 2: payment to Alice
+ const tx = await wallet.sendMTX(mtx);
+ assert(tx.verify(mtx.view));
+
+ // Wait for mempool and check
+ await waiter;
+ assert(hsd.mempool.hasEntry(tx.hash()));
+ })().then((hex) => {
+ ipc.send('claim-paid-transfer-reply', {});
+ }).catch((e) => {
+ ipc.send('claim-paid-transfer-reply', {
+ error: e.message,
+ });
+ });
+});
+
+function assert(value) {
+ if (!value) {
+ throw new Error('Assertion error.');
+ }
+}
diff --git a/app/pages/DomainManager/ClaimNameForPayment.js b/app/pages/DomainManager/ClaimNameForPayment.js
new file mode 100644
index 000000000..6b32e5062
--- /dev/null
+++ b/app/pages/DomainManager/ClaimNameForPayment.js
@@ -0,0 +1,162 @@
+import React, { Component } from 'react';
+import { MiniModal } from '../../components/Modal/MiniModal';
+import { MTX } from 'hsd/lib/primitives';
+import { connect } from 'react-redux';
+import { clientStub as nClientStub } from '../../background/node/client';
+import { showSuccess } from '../../ducks/notifications';
+
+const node = nClientStub(() => require('electron').ipcRenderer);
+
+@connect(
+ (state) => ({
+ network: state.node.network,
+ }),
+ (dispatch) => ({
+ showSuccess: (message) => dispatch(showSuccess(message)),
+ }),
+)
+export default class ClaimNameForPayment extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ step: 0,
+ hex: '',
+ };
+ }
+
+ onClickVerify = () => {
+ try {
+ const {network} = this.props;
+ const mtx = MTX.decode(Buffer.from(this.state.hex, 'hex'));
+ const firstOutput = mtx.outputs[0];
+ const nameReceiveAddr = firstOutput.address.toString(network);
+ const name = firstOutput.covenant.items[2].toString('ascii');
+ const secondOutput = mtx.outputs[1];
+ const fundingAddr = secondOutput.address.toString(network);
+ const price = secondOutput.value;
+
+ this.setState({
+ step: 1,
+ name,
+ nameReceiveAddr,
+ fundingAddr,
+ price,
+ });
+ } catch (e) {
+ this.setState({
+ hexError: 'Invalid hex value.',
+ });
+ }
+ };
+
+ onClickTransfer = async () => {
+ try {
+ await node.claimPaidTransfer(this.state.hex);
+ this.props.onClose();
+ this.props.showSuccess('Successfully claimed paid transfer. Please wait 1 block for the name to appear.');
+ } catch (e) {
+ this.setState({
+ transferError: e.message,
+ });
+ }
+ };
+
+ render() {
+ return (
+
+ If your counterparty has sent you a name transfer for payment, + paste the hex string into the box below to verify it and claim + your name. +
+ + {this.state.hexError && ( ++ {this.state.hexError} +
+ )} + ++ Please verify all the information below. If anything looks invalid, + please close this window. +
+ + {this.state.transferError && ( ++ {this.state.transferError} +
+ )} + +Send the hex string below to your counterparty.
+ ++ To require payment to finalize this transfer, please + fill out the steps below. +
+ +