Skip to content

Commit

Permalink
Support name transfers via @pinheadmz's name swap setup
Browse files Browse the repository at this point in the history
  • Loading branch information
mslipper committed Aug 21, 2020
1 parent 4f4cc73 commit 536ebfe
Show file tree
Hide file tree
Showing 9 changed files with 583 additions and 17 deletions.
4 changes: 3 additions & 1 deletion app/background/node/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ export const clientStub = ipcRendererInjector => makeClient(ipcRendererInjector,
'broadcastRawTx',
'sendRawAirdrop',
'getFees',
'getAverageBlockTime'
'getAverageBlockTime',
'finalizeWithPayment',
'claimPaidTransfer'
]);
36 changes: 36 additions & 0 deletions app/background/node/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down Expand Up @@ -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) {
Expand Down
124 changes: 123 additions & 1 deletion app/hsdMain.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
});
});

Expand All @@ -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

This comment has been minimized.

Copy link
@pinheadmz

pinheadmz Aug 21, 2020

Contributor

To make this robust in case someone is transferring a claimed name that had a weak flag, see https://github.com/handshake-org/hsd/blob/master/lib/wallet/wallet.js#L3032-L3045

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.

This comment has been minimized.

Copy link
@pinheadmz

pinheadmz Aug 21, 2020

Contributor

I think to verify alice's signature we can add something like:

const view = node.mempool.getCoinView(tx)
assert(mtx.verify(view))

... but I haven't checked this for sure. If Alice sends bad data, it just means the chain won't accept our finalize, which is a grief issue but still not fund or name loss.

Also we should check that the payment output uses the correct agreed-upon value (otherwise then Bob can lose money)

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:

This comment has been minimized.

Copy link
@pinheadmz

pinheadmz Aug 21, 2020

Contributor

nit SINGELREVERSE damn sorry my fault

//
// 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)

This comment has been minimized.

Copy link
@pinheadmz

pinheadmz Aug 21, 2020

Contributor

The mempool stuff here was really just for the testing suite, although I guess it is actually useful in this context as well!

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.');
}
}
162 changes: 162 additions & 0 deletions app/pages/DomainManager/ClaimNameForPayment.js
Original file line number Diff line number Diff line change
@@ -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 (
<MiniModal title="Claim Name For Payment" onClose={this.props.onClose}>
{this.renderSteps()}
</MiniModal>
);
}

renderSteps() {
switch (this.state.step) {
case 0:
return this.renderEnterHex();
case 1:
return this.renderVerify();
}
}

renderEnterHex() {
return (
<>
<p>
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.
</p>

{this.state.hexError && (
<p className="claim-name-for-payment-invalid">
{this.state.hexError}
</p>
)}

<div className="import-enter__textarea-container">
<textarea
className="import_enter_textarea"
value={this.state.hex}
onChange={(e) => this.setState({
hex: e.target.value,
})}
rows={8}
/>
</div>

<div className="send__actions">
<button
className="send__cta-btn"
onClick={this.onClickVerify}
disabled={!this.state.hex.length}
>
Verify
</button>
</div>
</>
);
}

renderVerify() {
return (
<>
<p>
Please verify all the information below. If anything looks invalid,
please close this window.
</p>

{this.state.transferError && (
<p className="claim-name-for-payment-invalid">
{this.state.transferError}
</p>
)}

<dl className="claim-name-for-payment-verification">
<dt>Name</dt>
<dd>{this.state.name}</dd>
<dt>Address Receiving Name</dt>
<dd>{this.state.nameReceiveAddr}</dd>
<dt>Address Receiving Funds</dt>
<dd>{this.state.fundingAddr}</dd>
<dt>Price</dt>
<dd>{this.state.price} HNS</dd>
</dl>

<div className="claim-name-for-payment__verification-buttons">
<button
className="abort"
onClick={this.props.onClose}
>
Abort
</button>
<button
className="pay-and-transfer"
onClick={this.onClickTransfer}
>
Pay and Transfer
</button>
</div>
</>
);
}
}
Loading

0 comments on commit 536ebfe

Please sign in to comment.