hive-multisign
Version:
A typescript utility for multi-signature transactions on the Hive blockchain
273 lines (272 loc) • 11.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.resetOriginalKey = exports.demo = exports.sendTrx = exports.hasEnoughSignatures = exports.prepareTrx = exports.assignUnifromAuth = exports.updateAuths = exports.generateAuthChangeObject = exports.generatePublicKeys = exports.generatePrivateKeys = exports.getUserAuthorities = void 0;
const dhive_1 = require("@hiveio/dhive");
const buffer_1 = require("buffer");
const client = new dhive_1.Client(['https://hive-api.arcange.eu', 'https://hived.emre.sh', 'https://api.hive.blog', 'https://api.openhive.network']);
/**
* Get account information
* @param usernames account usernames
* @returns
*/
const getAccounts = async (usernames) => {
return client.call('condenser_api', 'get_accounts', [usernames]);
};
/**
* Get user authorities (owner, active, posting)
* @param username
* @returns
*/
const getUserAuthorities = async (username) => {
const info = await getAccounts([username]);
if (info.length == 0) {
throw new Error(`Account @${username} not found`);
}
const account = info[0];
return {
owner: account.owner,
active: account.active,
posting: account.posting
};
};
exports.getUserAuthorities = getUserAuthorities;
/**
* Get the public memo key for the specified accounts
* @param username account username
* @returns
*/
const getMemoAndAccount = async (username) => {
let res = [];
const info = await getAccounts([username]);
if (info.length == 0) {
throw new Error(`Account @${username} not found`);
}
return {
owner: info[0].owner,
active: info[0].active,
posting: info[0].posting,
memo: info[0].memo_key
};
};
/**
* Generate many private keys for the account. Each private key is associated with one password.
* @param username account username
* @param passwords array of passwords, one of which must be the account's master password
* @param role key role (e.g. 'posting')
*/
const generatePrivateKeys = (username, passwords, role) => {
return passwords.map((pass) => dhive_1.PrivateKey.fromLogin(username, pass, role));
};
exports.generatePrivateKeys = generatePrivateKeys;
/**
* Generate the public keys associated with the specified private keys.
* @param privateKeys private keys
* @returns
*/
const generatePublicKeys = (privateKeys) => {
return privateKeys.map((key) => dhive_1.PrivateKey.from(key.toString()).createPublic().toString());
};
exports.generatePublicKeys = generatePublicKeys;
/**
* Generate the object needed to send an account authority update.
* @param username account username
* @param threshold weight threshold
* @param accountAuths account authorities [username, weight]
* @param keyAuths key authorities [public_key, weight]
* @param role key role (e.g. 'posting')
* @param replace Replace the old authorities instead of appending to them
* @returns
*/
const generateAuthChangeObject = async (username, threshold, accountAuths, keyAuths, role, replace) => {
const accountInfo = await getMemoAndAccount(username);
let newAccountAuths = Array.from(accountAuths);
let newKeyAuths = Array.from(keyAuths);
// Make sure the user is not sending public keys
for (let i = 0; i < newKeyAuths.length; i++) {
if (newKeyAuths[i][0][0] == '5') {
newKeyAuths[i][0] = (0, exports.generatePublicKeys)([newKeyAuths[i][0]])[0];
}
}
// If shouldn't replace account auths merge with the old ones
if (!(replace === null || replace === void 0 ? void 0 : replace.account)) {
const oldAccountAuths = accountInfo[role].account_auths;
newAccountAuths = mergeAuthWithoutDuplicates(oldAccountAuths, accountAuths);
}
// If shouldn't replace key auths merge with the old ones
if (!(replace === null || replace === void 0 ? void 0 : replace.key)) {
const oldKeyAuths = accountInfo[role].key_auths;
newKeyAuths = mergeAuthWithoutDuplicates(oldKeyAuths, keyAuths);
}
return {
account: username,
[role]: {
weight_threshold: threshold,
account_auths: newAccountAuths.sort((a, b) => (a[0] > b[0] ? 1 : -1)),
key_auths: newKeyAuths.sort((a, b) => (a[0] > b[0] ? 1 : -1))
},
json_metadata: '',
memo_key: accountInfo.memo
};
};
exports.generateAuthChangeObject = generateAuthChangeObject;
/**
* Merge authority pairs (account auths or key auths) considering newAuths the correct value in case of duplicates.
* @param oldAuths old authority pairs
* @param newAuths new authority pairs
* @returns
*/
const mergeAuthWithoutDuplicates = (oldAuths, newAuths) => {
const merged = Array.from(newAuths);
const setNew = new Set(newAuths.map((pair) => pair[0]));
// Add old auths that aren't duplicates
for (const pair of oldAuths) {
if (!setNew.has(pair[0])) {
merged.push(pair);
}
}
return merged;
};
/**
* Replace the chosen authority with the keys specified.
* @param username account username
* @param privateActive private active key
* @param threshold wight threshold required to broadcast a transaction
* @param keyAuths [public_key, weight]
* @param replace Replace the old authorities instead of appending to them
* @returns
*/
const updateAuths = async (username, privateActive, threshold, accountAuths, keyAuths, role, replace) => {
let sum = 0;
for (const keyWeight of keyAuths) {
sum += keyWeight[1];
}
if (sum < threshold) {
throw new Error(`Threshold (${threshold}) cannot be smaller than the sum of the weights (${sum})`);
}
const payload = await (0, exports.generateAuthChangeObject)(username, threshold, [], keyAuths, role, replace);
return client.broadcast.updateAccount(payload, dhive_1.PrivateKey.from(privateActive));
};
exports.updateAuths = updateAuths;
/**
* Add an authority for each password. Set the threshold such that a signature from each of them is required.
* @param username account username
* @param privateActive private active key
* @param passwords array of passowrds
* @param role key role (e.g. 'posting')
* @returns
*/
const assignUnifromAuth = (username, privateActive, passwords, role) => {
const keyAuths = (0, exports.generatePrivateKeys)(username, passwords, role).map((key) => [dhive_1.PrivateKey.from(key.toString()).createPublic().toString(), 1]);
return (0, exports.updateAuths)(username, privateActive, passwords.length, [], keyAuths, role, { key: true });
};
exports.assignUnifromAuth = assignUnifromAuth;
/**
* Prepare a transaction that needs to be signed.
* @param operations operations
* @param expireTime expire time [ms] in [0, 3590*1000]
* @returns
*/
const prepareTrx = async (operations, expireTime = 1000 * 3590) => {
const props = await client.database.getDynamicGlobalProperties();
const ref_block_num = props.head_block_number & 0xffff;
const ref_block_prefix = buffer_1.Buffer.from(props.head_block_id, 'hex').readUInt32LE(4);
const expiration = new Date(Date.now() + expireTime).toISOString().slice(0, -5);
const extensions = [];
return {
expiration,
extensions,
operations,
ref_block_num,
ref_block_prefix
};
};
exports.prepareTrx = prepareTrx;
/**
* Check if the specified transaction has enough signatures to be broadcasted to the blockchain.
* @param trx singed transaction
* @returns
*/
const hasEnoughSignatures = async (trx) => {
try {
return await client.database.verifyAuthority(trx);
}
catch (err) {
return false;
}
};
exports.hasEnoughSignatures = hasEnoughSignatures;
/**
* Sign transaction.
* @param trx transaction
* @param key private key
* @returns
*/
const signTrx = (trx, key) => {
return client.broadcast.sign(trx, key);
};
/**
* Broadcast a signed transaction.
* @param signedTrx signed transaction
* @returns
*/
const sendTrx = async (signedTrx) => {
return client.broadcast.send(signedTrx);
};
exports.sendTrx = sendTrx;
/**
* Test the multisign system. We reccomend testing on a disposable account.
* The test changes the posting authorities, requiring 2 signatures to send transactions and sends a test transaction to
* verify it works.
* IMPORTANT: this method will change your posting authority keys, if you want to go back to the original one you need to run
* the function 'resetOriginalKey'.
* @param username account username
* @param privateActive private active key
* @returns
*/
const demo = async (username, privateActive) => {
// Choose some passwords
const passwords = ['test123', 'test456'];
// Create private keys for those passwords
const newPrivates = (0, exports.generatePrivateKeys)(username, passwords, 'posting');
// Find the associated public keys
const newPublics = newPrivates.map((key) => dhive_1.PrivateKey.from(key.toString()).createPublic().toString());
// Create an array of [public_key, weight], where the weight specifies how much the key's vote is worth
// In this case our 2 keys will have both a weight of 2
const keyAuths = newPublics.map((key) => [key, 2]);
// Update the account. Use threshold = 4, so both signatures (2+2) are required for the transaction to be sent
await (0, exports.updateAuths)(username, privateActive, 4, [], keyAuths, 'posting', { key: true });
// Create an operation to check that the system works
const operation = [
'custom_json',
{
required_auths: [],
required_posting_auths: [username],
id: 'hive-multisign test',
json: JSON.stringify({ message: 'Transaction broadcasted with multiple signatures' })
}
];
// Prepare the transaction to be sent
const trx = await (0, exports.prepareTrx)([operation]);
// Sign the transaction with all the keys
let signedTrx = {};
// Sign with the first private key
signedTrx = client.broadcast.sign(trx, dhive_1.PrivateKey.from(newPrivates[0].toString()));
// Sign with the second private key the partially signed trx
signedTrx = client.broadcast.sign(signedTrx, dhive_1.PrivateKey.from(newPrivates[1].toString()));
// Send the transaction
return client.broadcast.send(signedTrx);
};
exports.demo = demo;
/**
* Reset your posting key to the original. If you can't find your master password don't worry, choose a password now and
* then go to your wallet and change your posting key to the one generated here.
* @param username account username
* @param privateActive private active key
* @param masterPassword master password, it was given to you along with the other keys
*/
const resetOriginalKey = async (username, privateActive, masterPassword) => {
const newPrivate = (0, exports.generatePrivateKeys)(username, [masterPassword], 'posting')[0];
await (0, exports.updateAuths)(username, privateActive, 1, [], [[dhive_1.PrivateKey.from(newPrivate.toString()).createPublic().toString(), 1]], 'posting', { key: true });
console.log(`Your new private key is: ${newPrivate.toString()}`);
};
exports.resetOriginalKey = resetOriginalKey;