Skip to content

Instantly share code, notes, and snippets.

@rithvikvibhu
Created August 27, 2021 18:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rithvikvibhu/cc23a7f42f27e062d35c9e67e6df3756 to your computer and use it in GitHub Desktop.
Save rithvikvibhu/cc23a7f42f27e062d35c9e67e6df3756 to your computer and use it in GitHub Desktop.
Pre-signed OPEN, BID, REVEAL transactions
/**
* Generate pre-signed OPEN, BID, REVEAL transactions
*
* TX 1 (immediately broadcast):
* 1 (or more) coin (100 HNS) -----> OPEN name/ (0 HNS)
* \----> addr2 (lockup value = 70.1)
* \----> change (29.9 HNS - fee)
*
* TX 2 (broadcast in BIDDING period):
* addr2 (70.1 HNS) -----> BID name/ (70)
* \----> change (0.1 - fee)
*
* TX 3 (broadcast in REVEAL period):
* BID name/ (70) -----> REVEAL /name (30)
* \----> change (blind of 40 - fee)
*/
'use strict';
const FullNode = require('hsd/lib/node/fullnode');
const MTX = require('hsd/lib/primitives/mtx');
const Output = require('hsd/lib/primitives/output');
const Coin = require('hsd/lib/primitives/coin');
const Outpoint = require('hsd/lib/primitives/outpoint');
const { states } = require('hsd/lib/covenants/namestate');
const rules = require('hsd/lib/covenants/rules');
const { types } = rules;
const { NodeClient, WalletClient } = require('hs-client');
const nodeClient = new NodeClient({
port: 14037,
});
const walletClient = new WalletClient({
port: 14039,
timeout: 30000,
});
let node, wallet;
async function mineBlocks(num = 1) {
const addr = await walletClient.execute('getnewaddress', []);
await nodeClient.execute('generatetoaddress', [num, addr]);
}
async function setup() {
node = new FullNode({
prefix: '/tmp/hsd_data',
network: 'regtest',
plugins: [require('hsd/lib/wallet/plugin')],
logConsole: true,
logLevel: 'debug',
});
const { wdb } = node.require('walletdb');
// console.log('node', node);
// console.log('wdb', wdb);
await node.ensure();
await node.open();
wallet = await wdb.primary;
await nodeClient.open();
await walletClient.open();
console.log('funding wallet...');
await mineBlocks(30);
}
/**
* Make Open MTX
* Same as makeOpen, but adds an extra output of given value
* @returns openMtx
* */
async function makeOpenToNewAddr(name, acct, newAddrValue) {
// assert(typeof name === 'string');
// assert(acct >>> 0 === acct || typeof acct === 'string');
if (!rules.verifyName(name)) throw new Error('Invalid name.');
const rawName = Buffer.from(name, 'ascii');
const nameHash = rules.hashName(rawName);
const height = wallet.wdb.height + 1;
const network = wallet.network;
// TODO: Handle expired behavior.
if (rules.isReserved(nameHash, height, network))
throw new Error('Name is reserved.');
if (!rules.hasRollout(nameHash, height, network))
throw new Error('Name not yet available.');
let ns = await wallet.getNameState(nameHash);
if (!ns) ns = await wallet.wdb.getNameStatus(nameHash);
ns.maybeExpire(height, network);
const state = ns.state(height, network);
const start = ns.height;
if (state !== states.OPENING) throw new Error('Name is not available.');
if (start !== 0 && start !== height)
throw new Error('Name is already opening.');
const addr = await wallet.receiveAddress(acct);
// TX 1 (open)
const output1 = new Output();
output1.address = addr;
output1.value = 0;
output1.covenant.type = types.OPEN;
output1.covenant.pushHash(nameHash);
output1.covenant.pushU32(0);
output1.covenant.push(rawName);
const output2 = new Output();
output2.address = addr;
output2.value = newAddrValue;
output2.covenant.type = types.NONE;
const mtx = new MTX();
mtx.outputs.push(output1);
mtx.outputs.push(output2);
if (await wallet.txdb.isDoubleOpen(mtx))
throw new Error(`Already sent an open for: ${name}.`);
return mtx;
}
/**
* Make Bid MTX
* Same as makeBid, but does not require the auction to be open
* @returns bidMtx
* */
async function makeBid(name, value, lockup, acct) {
// assert(typeof name === 'string');
// assert(Number.isSafeInteger(value) && value >= 0);
// assert(Number.isSafeInteger(lockup) && lockup >= 0);
// assert((acct >>> 0) === acct || typeof acct === 'string');
if (!rules.verifyName(name)) throw new Error('Invalid name.');
const rawName = Buffer.from(name, 'ascii');
const nameHash = rules.hashName(rawName);
const height = wallet.wdb.height + 1;
const network = wallet.network;
if (rules.isReserved(nameHash, height, network))
throw new Error('Name is reserved.');
if (!rules.hasRollout(nameHash, height, network))
throw new Error('Name not yet available.');
let ns = await wallet.getNameState(nameHash);
if (!ns) ns = await wallet.wdb.getNameStatus(nameHash);
ns.maybeExpire(height, network);
const state = ns.state(height, network);
const start = ns.height;
// if (state === states.OPENING)
// throw new Error('Name has not reached the bidding phase yet.');
// if (state !== states.BIDDING) throw new Error('Name is not available.');
if (value > lockup) throw new Error('Bid exceeds lockup value.');
const addr = await wallet.receiveAddress(acct);
const blind = await wallet.generateBlind(nameHash, addr, value);
const output = new Output();
output.address = addr;
output.value = lockup;
output.covenant.type = types.BID;
output.covenant.pushHash(nameHash);
output.covenant.pushU32(start);
output.covenant.push(rawName);
output.covenant.pushHash(blind);
const mtx = new MTX();
mtx.outputs.push(output);
return mtx;
}
/**
* Make Reveal MTX
* Same as makeReveal, but uses data from a bid coin (mtx, index)
* @returns revealMtx
* */
async function makeReveal(bidMtx, bidOutputIndex) {
const bidOutput = bidMtx.outputs[bidOutputIndex];
const bidCoin = Coin.fromTX(bidMtx, bidOutputIndex, -1);
const nameHash = bidOutput.covenant.getHash(0);
const height = bidOutput.covenant.getU32(1);
const blind = bidOutput.covenant.getHash(3);
const bv = await wallet.getBlind(blind);
if (!bv) throw new Error('Blind value not found.');
const { nonce, value } = bv;
const output = new Output();
output.address = bidCoin.address;
output.value = value;
output.covenant.type = types.REVEAL;
output.covenant.pushHash(nameHash);
output.covenant.pushU32(height);
output.covenant.pushHash(nonce);
const mtx = new MTX();
mtx.outputs.push(output);
return mtx;
}
/**
* Create Auction Transactions
* Creates and signs open, bid, reveal txs
* @returns object { openMtx, bidMtx, revealMtx }
* */
async function createOpenBidReveal(name, bid, blind) {
// Calculated
const acct = 'default';
const bidTxFee = 0.05 * 1e6;
const revealTxFee = 0.05 * 1e6;
const newAddrValue = bid + blind + bidTxFee + revealTxFee;
let openMtx, bidMtx, revealMtx;
const unlock = await wallet.fundLock.lock();
try {
// First: OPEN tx
console.log('Creating OPEN tx...');
openMtx = await makeOpenToNewAddr(name, acct, newAddrValue);
await wallet.fill(openMtx);
openMtx = await wallet.finalize(openMtx);
console.log('TX 1:', openMtx.toJSON());
// Second: BID tx
console.log('Creating BID tx...');
const newAddrOutputIndex = openMtx.outputs.findIndex(
(o) => o.value === newAddrValue
);
bidMtx = await makeBid(name, bid, bid + blind + revealTxFee, acct);
bidMtx.addOutpoint(Outpoint.fromTX(openMtx, newAddrOutputIndex));
await wallet.fill(bidMtx, {
coins: [Coin.fromTX(openMtx, newAddrOutputIndex, -1)],
});
bidMtx = await wallet.finalize(bidMtx);
console.log('TX 2:', bidMtx.toJSON());
// Third: REVEAL tx
console.log('Creating REVEAL tx...');
const bidOutputIndex = bidMtx.outputs.findIndex((o) => o.covenant.isBid());
revealMtx = await makeReveal(bidMtx, bidOutputIndex);
revealMtx.addOutpoint(Outpoint.fromTX(bidMtx, bidOutputIndex));
await wallet.fill(revealMtx, {
coins: [Coin.fromTX(bidMtx, bidOutputIndex, -1)],
});
revealMtx = await wallet.finalize(revealMtx);
console.log('TX 3:', revealMtx.toJSON());
} finally {
unlock();
}
// Sign all 3 transactions
console.log('Signing all transactions...');
await wallet.sign(openMtx, null);
console.log('Signed TX 1:', openMtx);
await wallet.sign(bidMtx, null);
console.log('Signed TX 2:', bidMtx);
await wallet.sign(revealMtx, null);
console.log('Signed TX 3:', revealMtx);
return { openMtx, bidMtx, revealMtx };
}
/**
* Driver code
* Runs the whole auction flow
* @returns openMtx
* */
(async () => {
await setup();
// const primaryWallet = walletClient.wallet('primary');
// console.log(await primaryWallet.getInfo());
// console.log(await primaryWallet.getAccount('default'));
// User input
const name = 'a';
const bid = 12 * 1e6;
const blind = 6 * 1e6;
// Create the 3 transactions
const { openMtx, bidMtx, revealMtx } = await createOpenBidReveal(
name,
bid,
blind
);
const rawName = Buffer.from(name, 'ascii');
const nameHash = rules.hashName(rawName);
const ns = await node.getNameStatus(nameHash);
let height, state;
// Ensure ns state is closed
height = wallet.wdb.height + 1;
state = ns.state(height, wallet.network);
if (state !== states.OPENING && ns.height !== 0 && ns.height !== height) {
throw new Error('Name should not be available for opening.');
}
// Send OPEN immediately
height = wallet.wdb.height + 1;
console.log(`Height: ${height}`, 'Broadcasting OPEN now...');
await nodeClient.broadcast(openMtx.toHex());
await mineBlocks(1);
height = wallet.wdb.height + 1;
console.log(`Height: ${height}`);
// ns state must be opening now
state = ns.state(height, wallet.network);
if (state !== states.OPENING && 0 < ns.height < height) {
throw new Error('Name should be opening now.');
}
// Skip to bidding period
await mineBlocks(6);
height = wallet.wdb.height + 1;
console.log(`Height: ${height}`);
// Send BID
height = wallet.wdb.height + 1;
console.log(`Height: ${height}`, 'Broadcasting BID now...');
await nodeClient.broadcast(bidMtx.toHex());
await mineBlocks(1);
height = wallet.wdb.height + 1;
console.log(`Height: ${height}`);
const bids = await wallet.getBidsByName(name);
console.log('bids:', bids);
if (bids.length !== 1) {
throw new Error('Exactly 1 bid should be found.');
}
// Skip to reveal period
await mineBlocks(10);
height = wallet.wdb.height + 1;
console.log(`Height: ${height}`);
// Send REVEAL
height = wallet.wdb.height + 1;
console.log(`Height: ${height}`, 'Broadcasting REVEAL now...');
await nodeClient.broadcast(revealMtx.toHex());
await mineBlocks(1);
height = wallet.wdb.height + 1;
console.log(`Height: ${height}`);
const reveals = await wallet.getRevealsByName(name);
console.log('reveals:', reveals);
if (reveals.length !== 1) {
throw new Error('Exactly 1 reveal should be found.');
}
// Check all transactions
const txs = await wallet.getHistory();
console.log('txs from wallet:');
console.log(
txs.map((t) => t.tx.toJSON()).filter((tx) => tx.outputs.length > 1)
);
console.log('done.');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment