UNPKG

banani

Version:

JS/TS library for the Banano cryptocurrency in the style of bananopie

297 lines (296 loc) 14 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PrivateKeyAccount = exports.Wallet = void 0; const util = __importStar(require("./util")); /** wallets are created from seeds, so they can have multiple addresses by changing the index. use wallets to "write" (send, receive, change rep) to the network */ class Wallet { /** * @param {string} [seed] Seed for the wallet from which private keys are derived. 64 character hex string (32 bytes) */ constructor(rpc, seed, index = 0, work_function) { this.add_do_work = true; this.rpc = rpc; if (typeof seed !== "string" || seed?.length !== 64) throw Error("Seed needs to be 64 character (hex) string"); this.seed = seed; this.index = index; this.work_function = work_function; } /** Generate a cryptographically secure random wallet using [crypto.getRandomValues](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) */ static gen_random_wallet(rpc) { let random_bytes = new Uint8Array(32); crypto.getRandomValues(random_bytes); const random_seed = util.uint8array_to_hex(random_bytes); return new Wallet(rpc, random_seed); } //Own properties get private_key() { return util.get_private_key_from_seed(this.seed, this.index); } get public_key() { return util.get_public_key_from_private_key(this.private_key); } get address() { return util.get_address_from_public_key(this.public_key); } //Actions async send_process(block, subtype) { return (await this.rpc.call({ action: "process", json_block: "true", subtype, block, do_work: !block.work && this.add_do_work ? true : undefined, })).hash; } /** * @param {Address} [to] address to send to * @param {util.Whole} [amount] amount in whole bananos to send * @param {boolean?} [gen_work] whether or not to call work function to generate work * @param {string?} [representative] optionally provide a representative if you do not want to use the current representative * @param {AccountInfoRPC?} [cached_account_info] can save one rpc call in some cases. Mostly for internal use. Make sure that in the RPC call, "representative" is "true" Send Bananos */ async send(to, amount, gen_work, representative, cached_account_info) { const raw_send = util.whole_to_raw(amount, this.rpc.DECIMALS); const info = cached_account_info ?? (await this.get_account_info(undefined, true)); //this should be lazy. the true makes sure representative is included const pub_receive = util.get_public_key_from_address(to); if (representative === undefined) { if (info.representative === undefined) throw Error("Missing field 'representative' in `cached_account_info`"); representative = info.representative; } const before_balance = BigInt(info.balance); const new_balance = before_balance - raw_send; if (new_balance < 0n) { throw Error(`Insufficient funds to send. Cannot send more than balance; ie, Before balance (raw: ${before_balance}) less than send amount (raw: ${raw_send})`); } const block_ns = { type: "state", account: this.address, previous: info.frontier, representative, balance: new_balance.toString(), //you gotta trust me here typescript //link is public key of account to send to link: pub_receive, link_as_account: to, }; const s_block_hash = util.hash_block(block_ns); //block hash of the send block let work = undefined; if (gen_work && this.work_function) work = await this.work_function(info.frontier); const signature = util.sign_block_hash(this.private_key, s_block_hash); const block = { ...block_ns, signature, work }; return await this.send_process(block, "send"); } /** Send by passing in a fixed final balance */ async send_fixed_final_bal(to, end_bal, gen_work, representative, cached_account_info) { const raw_end = util.whole_to_raw(end_bal, this.rpc.DECIMALS); const info = cached_account_info ?? (await this.get_account_info(undefined, true)); //this should be lazy. the true makes sure representative is included const pub_receive = util.get_public_key_from_address(to); if (representative === undefined) { if (info.representative === undefined) throw Error("Missing field 'representative' in `cached_account_info`"); representative = info.representative; } if (raw_end < 0n) { throw Error(`End balance cannot be negative`); } const block_ns = { type: "state", account: this.address, previous: info.frontier, representative, balance: raw_end.toString(), //you gotta trust me here typescript //link is public key of account to send to link: pub_receive, link_as_account: to, }; const s_block_hash = util.hash_block(block_ns); //block hash of the send block let work = undefined; if (gen_work && this.work_function) work = await this.work_function(info.frontier); const signature = util.sign_block_hash(this.private_key, s_block_hash); const block = { ...block_ns, signature, work }; return await this.send_process(block, "send"); } /* Send all Bananos */ async send_all(to, work, representative) { const info = await this.get_account_info(undefined, true); return await this.send(to, util.raw_to_whole(BigInt(info.balance), this.rpc.DECIMALS), work, representative, info); } /** * @param {BlockHash} [block_hash] send block to receive * @param {boolean?} [gen_work] whether or not to call work function to generate work * @param {Address?} [representative] optionally provide a representative if you do not want to use the current representative receive bananos from a specific send block */ async receive(block_hash, gen_work, representative) { //doesn't matter if open or not, I think? const block_info = await this.rpc.get_block_info(block_hash); let before_balance = 0n; let previous; try { const info = await this.get_account_info(undefined, true); previous = info.frontier; if (!representative) representative = info.representative; before_balance = BigInt(info.balance); } catch (e) { //todo, check if error message is "Account not found" //console.log(e) //unopened account probably previous = "0".repeat(64); } if (representative === undefined) representative = this.address; const block_ns = { type: "state", account: this.address, previous, representative, // prettier-ignore balance: (before_balance + BigInt(block_info.amount)).toString(), //link is hash of send block link: block_hash, }; const r_block_hash = util.hash_block(block_ns); //block hash of the receive block let work = undefined; if (gen_work && this.work_function) work = await this.work_function(previous === "0".repeat(64) ? this.public_key : previous); const signature = util.sign_block_hash(this.private_key, r_block_hash); const block = { ...block_ns, signature, work }; return await this.send_process(block, "receive"); } //todo: might have some error with multiple receives? /** * @param {number} [count=20] Max amount of blocks to receive receive all (up to count and exceeding threshold if applicable) receivable blocks * @param {`${number}`?} [threshold] Min amount of Banano to receive in whole * @param {boolean?} [gen_work] whether or not to call work function to generate work Receive all receivable transactions (up to count, and over threshold */ async receive_all(count = 20, threshold, gen_work) { const to_receive = (await this.get_account_receivable(count, threshold, true)).blocks; let previous, representative, before_balance; try { const info = await this.get_account_info(undefined, true); previous = info.frontier; representative = info.representative; before_balance = BigInt(info.balance); } catch (e) { //todo, check if error message is "Account not found" //console.log(e) //unopened account probably previous = "0".repeat(64); before_balance = BigInt(0); } if (representative === undefined) representative = this.address; let receive_block_hashes = []; for (const receive_hash of Object.keys(to_receive)) { const new_balance = (before_balance + BigInt(to_receive[receive_hash].amount)).toString(); const block_ns = { type: "state", account: this.address, previous, representative, balance: new_balance, //link is hash of send block link: receive_hash, }; const r_block_hash = util.hash_block(block_ns); //block hash of the receive block let work = undefined; if (gen_work && this.work_function) work = await this.work_function(previous === "0".repeat(64) ? this.public_key : previous); const signature = util.sign_block_hash(this.private_key, r_block_hash); const block = { ...block_ns, signature, work }; await this.send_process(block, "receive"); receive_block_hashes.push(r_block_hash); previous = r_block_hash; before_balance = BigInt(new_balance); } return receive_block_hashes; } /** * @param {Address} [new_representative] banano address to change representative to * @param {boolean?} [gen_work] whether or not to call work function to generate work */ async change_representative(new_representative, gen_work) { const info = await this.get_account_info(); const block_ns = { type: "state", account: this.address, previous: info.frontier, representative: new_representative, balance: info.balance, //link is 0 link: "0".repeat(64), }; const c_block_hash = util.hash_block(block_ns); //block hash of the change block let work = undefined; if (gen_work && this.work_function) work = await this.work_function(info.frontier); const signature = util.sign_block_hash(this.private_key, c_block_hash); const block = { ...block_ns, signature, work }; return await this.send_process(block, "change"); } /* alias for the change_representative method */ async change_rep(new_representative, work) { return await this.change_representative(new_representative, work); } //Double wrapped functions async get_account_info(include_confirmed, representative, weight, pending) { return await this.rpc.get_account_info(this.address, include_confirmed, representative, weight, pending); } async get_account_receivable(count, threshold, source) { return await this.rpc.get_account_receivable(this.address, count, threshold, source); } // /* Sign a message with the current private key. Signing is a way to cryptographically prove that someone posesses a certain private key without revealing the actual private key */ sign_message(message) { return util.sign_message(this.private_key, message); } } exports.Wallet = Wallet; /** Does everything a `Wallet` can do, except a private key is put in instead of a seed, and so limited to one address. Means changing `.index` will not do anything obviously. */ class PrivateKeyAccount extends Wallet { /** * @param {string} [private_key] Private key. 64 character hex string (32 bytes) */ constructor(rpc, private_key, work_function) { if (typeof private_key !== "string" || private_key?.length !== 64) throw Error("Priv key needs to be 64 character (hex) string"); super(rpc, private_key, 0, work_function); this._private_key = private_key; } get private_key() { return this._private_key; } } exports.PrivateKeyAccount = PrivateKeyAccount;