Created
August 27, 2021 18:58
-
-
Save rithvikvibhu/cc23a7f42f27e062d35c9e67e6df3756 to your computer and use it in GitHub Desktop.
Pre-signed OPEN, BID, REVEAL transactions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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