UNPKG

hive-multisign

Version:

A typescript utility for multi-signature transactions on the Hive blockchain

316 lines (286 loc) 11.3 kB
import { AuthorityType, Client, KeyRole, Operation, PrivateKey, PublicKey, SignedTransaction, Transaction, TransactionConfirmation } from '@hiveio/dhive' import { Buffer } from 'buffer' const client = new Client(['https://hive-api.arcange.eu', 'https://hived.emre.sh', 'https://api.hive.blog', 'https://api.openhive.network']) export interface IAccountAuthority { weight_threshold: number account_auths?: [string, number][] key_auths?: [string, number][] } /** * Get account information * @param usernames account usernames * @returns */ const getAccounts = async (usernames: string[]): Promise<any> => { return client.call('condenser_api', 'get_accounts', [usernames]) } /** * Get user authorities (owner, active, posting) * @param username * @returns */ export const getUserAuthorities = async (username: string): Promise<{ owner: IAccountAuthority; active: IAccountAuthority; posting: IAccountAuthority }> => { 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 } } /** * Get the public memo key for the specified accounts * @param username account username * @returns */ const getMemoAndAccount = async ( username: string ): Promise<{ owner: IAccountAuthority; active: IAccountAuthority; posting: IAccountAuthority; memo: string }> => { let res: string[] = [] 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') */ export const generatePrivateKeys = (username: string, passwords: string[], role: KeyRole): PrivateKey[] => { return passwords.map((pass: string) => PrivateKey.fromLogin(username, pass, role)) } /** * Generate the public keys associated with the specified private keys. * @param privateKeys private keys * @returns */ export const generatePublicKeys = (privateKeys: string[] | PrivateKey[]): string[] => { return privateKeys.map((key) => PrivateKey.from(key.toString()).createPublic().toString()) } /** * 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 */ export const generateAuthChangeObject = async ( username: string, threshold: number, accountAuths: [string, number][], keyAuths: [string, number][], role: KeyRole, replace?: { account?: boolean; key?: boolean } ): Promise<{ account: string owner?: AuthorityType | undefined active?: AuthorityType | undefined posting?: AuthorityType | undefined memo_key: string | PublicKey json_metadata: string }> => { 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] = generatePublicKeys([newKeyAuths[i][0]])[0] } } // If shouldn't replace account auths merge with the old ones if (!replace?.account) { const oldAccountAuths = (accountInfo[role] as IAccountAuthority).account_auths as [string, number][] newAccountAuths = mergeAuthWithoutDuplicates(oldAccountAuths, accountAuths) } // If shouldn't replace key auths merge with the old ones if (!replace?.key) { const oldKeyAuths = (accountInfo[role] as IAccountAuthority).key_auths as [string, number][] 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 as string } } /** * 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: [string, number][], newAuths: [string, number][]): [string, number][] => { 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 */ export const updateAuths = async ( username: string, privateActive: string, threshold: number, accountAuths: [string, number][], keyAuths: [string, number][], role: KeyRole, replace?: { account?: boolean; key?: boolean } ): Promise<TransactionConfirmation> => { 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 generateAuthChangeObject(username, threshold, [], keyAuths, role, replace) return client.broadcast.updateAccount(payload, PrivateKey.from(privateActive)) } /** * 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 */ export const assignUnifromAuth = (username: string, privateActive: string, passwords: string[], role: KeyRole) => { const keyAuths = generatePrivateKeys(username, passwords, role).map((key: PrivateKey) => [PrivateKey.from(key.toString()).createPublic().toString(), 1]) return updateAuths(username, privateActive, passwords.length, [], keyAuths as [string, number][], role, { key: true }) } /** * Prepare a transaction that needs to be signed. * @param operations operations * @param expireTime expire time [ms] in [0, 3590*1000] * @returns */ export const prepareTrx = async (operations: Operation[], expireTime: number = 1000 * 3590): Promise<Transaction> => { const props = await client.database.getDynamicGlobalProperties() const ref_block_num = props.head_block_number & 0xffff const ref_block_prefix = Buffer.from(props.head_block_id, 'hex').readUInt32LE(4) const expiration = new Date(Date.now() + expireTime).toISOString().slice(0, -5) const extensions: any = [] return { expiration, extensions, operations, ref_block_num, ref_block_prefix } } /** * Check if the specified transaction has enough signatures to be broadcasted to the blockchain. * @param trx singed transaction * @returns */ export const hasEnoughSignatures = async (trx: SignedTransaction): Promise<boolean> => { try { return await client.database.verifyAuthority(trx) } catch (err) { return false } } /** * Sign transaction. * @param trx transaction * @param key private key * @returns */ const signTrx = (trx: Transaction, key: PrivateKey) => { return client.broadcast.sign(trx, key) } /** * Broadcast a signed transaction. * @param signedTrx signed transaction * @returns */ export const sendTrx = async (signedTrx: SignedTransaction): Promise<TransactionConfirmation> => { return client.broadcast.send(signedTrx) } /** * 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 */ export const demo = async (username: string, privateActive: string) => { // Choose some passwords const passwords: string[] = ['test123', 'test456'] // Create private keys for those passwords const newPrivates: PrivateKey[] = generatePrivateKeys(username, passwords, 'posting') // Find the associated public keys const newPublics: string[] = newPrivates.map((key) => 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: [string, number][] = 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 updateAuths(username, privateActive, 4, [], keyAuths, 'posting', { key: true }) // Create an operation to check that the system works const operation: 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 prepareTrx([operation]) // Sign the transaction with all the keys let signedTrx: any = {} // Sign with the first private key signedTrx = client.broadcast.sign(trx, PrivateKey.from(newPrivates[0].toString())) // Sign with the second private key the partially signed trx signedTrx = client.broadcast.sign(signedTrx, PrivateKey.from(newPrivates[1].toString())) // Send the transaction return client.broadcast.send(signedTrx as SignedTransaction) } /** * 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 */ export const resetOriginalKey = async (username: string, privateActive: string, masterPassword: string) => { const newPrivate: PrivateKey = generatePrivateKeys(username, [masterPassword], 'posting')[0] await updateAuths(username, privateActive, 1, [], [[PrivateKey.from(newPrivate.toString()).createPublic().toString(), 1]], 'posting', { key: true }) console.log(`Your new private key is: ${newPrivate.toString()}`) }