|
| 1 | +# coding=utf-8 |
| 2 | +from __future__ import absolute_import, division, print_function, \ |
| 3 | + unicode_literals |
| 4 | + |
| 5 | +import codecs |
| 6 | +import json |
| 7 | +from argparse import ArgumentParser |
| 8 | +from pprint import pformat |
| 9 | +from subprocess import PIPE, run |
| 10 | +from typing import List, Optional, Text |
| 11 | + |
| 12 | +import filters as f |
| 13 | +from six import binary_type, text_type |
| 14 | + |
| 15 | +from iota import Bundle, Iota, TransactionTrytes |
| 16 | +from iota.bin import IotaCommandLineApp |
| 17 | +from iota.json import JsonEncoder |
| 18 | +from iota.crypto.addresses import AddressGenerator |
| 19 | +from iota.filters import Trytes |
| 20 | + |
| 21 | + |
| 22 | +class IotaMamExample(IotaCommandLineApp): |
| 23 | + """ |
| 24 | + Shows how to integrate the ``mam.client.js`` Javascript library into a |
| 25 | + Python script, until MAM functionality is implemented in PyOTA. |
| 26 | +
|
| 27 | + In order to execute this script, you must install Node and the |
| 28 | + ``mam.client.js`` library. |
| 29 | +
|
| 30 | + See https://github.com/iotaledger/mam.client.js for more information. |
| 31 | + """ |
| 32 | + def execute(self, api, **arguments): |
| 33 | + # type: (Iota, ...) -> int |
| 34 | + channel_key_index = arguments['channel_key_index'] # type: int |
| 35 | + count = arguments['count'] # type: int |
| 36 | + depth = arguments['depth'] # type: int |
| 37 | + dry_run = arguments['dry_run'] # type: bool |
| 38 | + mam_encrypt_path = arguments['mam_encrypt_path'] # type: Text |
| 39 | + min_weight_magnitude = arguments['min_weight_magnitude'] # type: int |
| 40 | + message_encoding = arguments['message_encoding'] # type: Text |
| 41 | + message_file = arguments['message_file'] # type: Optional[Text] |
| 42 | + security_level = arguments['security_level'] # type: int |
| 43 | + start = arguments['start'] # type: int |
| 44 | + |
| 45 | + if message_file: |
| 46 | + with codecs.open(message_file, 'r', message_encoding) as f_: # type: codecs.StreamReaderWriter |
| 47 | + message = f_.read() |
| 48 | + |
| 49 | + else: |
| 50 | + self.stdout.write( |
| 51 | + 'Enter message to send. Press Ctrl-D on a blank line when done.\n\n', |
| 52 | + ) |
| 53 | + |
| 54 | + message = self.stdin.read().strip() |
| 55 | + self.stdout.write('\n') |
| 56 | + |
| 57 | + # Generating the encrypted message may take a little while, so we |
| 58 | + # should provide some feedback to the user so that they know that |
| 59 | + # their input is being processed (this is especially important if |
| 60 | + # the user typed in their message, so that they don't press ^D |
| 61 | + # again, thinking that the program didn't register the first one). |
| 62 | + self.stdout.write('Encrypting message...\n') |
| 63 | + |
| 64 | + proc =\ |
| 65 | + run( |
| 66 | + args = [ |
| 67 | + # mam_encrypt.js |
| 68 | + mam_encrypt_path, |
| 69 | + |
| 70 | + # Required arguments |
| 71 | + binary_type(api.seed), |
| 72 | + message, |
| 73 | + |
| 74 | + # Options |
| 75 | + '--channel-key-index', text_type(channel_key_index), |
| 76 | + '--start', text_type(start), |
| 77 | + '--count', text_type(count), |
| 78 | + '--security-level', text_type(security_level), |
| 79 | + ], |
| 80 | + |
| 81 | + check = True, |
| 82 | + stdout = PIPE, |
| 83 | + stderr = self.stderr, |
| 84 | + ) |
| 85 | + |
| 86 | + # The output of the JS script is a collection of transaction |
| 87 | + # trytes, encoded as JSON. |
| 88 | + filter_ =\ |
| 89 | + f.FilterRunner( |
| 90 | + starting_filter = |
| 91 | + f.Required |
| 92 | + | f.Unicode |
| 93 | + | f.JsonDecode |
| 94 | + | f.Array |
| 95 | + | f.FilterRepeater( |
| 96 | + f.ByteString(encoding='ascii') |
| 97 | + | Trytes(result_type=TransactionTrytes) |
| 98 | + ), |
| 99 | + |
| 100 | + incoming_data = proc.stdout, |
| 101 | + ) |
| 102 | + |
| 103 | + if not filter_.is_valid(): |
| 104 | + self.stderr.write( |
| 105 | + 'Invalid output from {mam_encrypt_path}:\n' |
| 106 | + '\n' |
| 107 | + 'Output:\n' |
| 108 | + '{output}\n' |
| 109 | + '\n' |
| 110 | + 'Errors:\n' |
| 111 | + '{errors}\n'.format( |
| 112 | + errors = pformat(filter_.get_errors(with_context=True)), |
| 113 | + mam_encrypt_path = mam_encrypt_path, |
| 114 | + output = proc.stdout, |
| 115 | + ), |
| 116 | + ) |
| 117 | + |
| 118 | + return 2 |
| 119 | + |
| 120 | + transaction_trytes = filter_.cleaned_data # type: List[TransactionTrytes] |
| 121 | + |
| 122 | + if dry_run: |
| 123 | + bundle = Bundle.from_tryte_strings(transaction_trytes) |
| 124 | + |
| 125 | + self.stdout.write('Transactions:\n\n') |
| 126 | + self.stdout.write(json.dumps(bundle, cls=JsonEncoder, indent=2)) |
| 127 | + else: |
| 128 | + api.send_trytes( |
| 129 | + depth = depth, |
| 130 | + trytes = transaction_trytes, |
| 131 | + min_weight_magnitude = min_weight_magnitude, |
| 132 | + ) |
| 133 | + |
| 134 | + return 0 |
| 135 | + |
| 136 | + def create_argument_parser(self): |
| 137 | + # type: () -> ArgumentParser |
| 138 | + parser = super(IotaMamExample, self).create_argument_parser() |
| 139 | + |
| 140 | + parser.add_argument( |
| 141 | + 'mam_encrypt_path', |
| 142 | + |
| 143 | + help = 'Path to `mam_encrypt.js` script.', |
| 144 | + ) |
| 145 | + |
| 146 | + parser.add_argument( |
| 147 | + '--channel-key-index', |
| 148 | + default = 0, |
| 149 | + dest = 'channel_key_index', |
| 150 | + type = int, |
| 151 | + |
| 152 | + help = 'Index of the key used to establish the channel.', |
| 153 | + ) |
| 154 | + |
| 155 | + parser.add_argument( |
| 156 | + '--start', |
| 157 | + default = 0, |
| 158 | + type = int, |
| 159 | + |
| 160 | + help = 'Index of the first key used to encrypt the message.', |
| 161 | + ) |
| 162 | + |
| 163 | + parser.add_argument( |
| 164 | + '--count', |
| 165 | + default = 1, |
| 166 | + type = int, |
| 167 | + |
| 168 | + help = 'Number of keys to use to encrypt the message.', |
| 169 | + ) |
| 170 | + |
| 171 | + parser.add_argument( |
| 172 | + '--security-level', |
| 173 | + default = AddressGenerator.DEFAULT_SECURITY_LEVEL, |
| 174 | + type = int, |
| 175 | + |
| 176 | + help = 'Number of iterations to use when generating keys.', |
| 177 | + ) |
| 178 | + |
| 179 | + parser.add_argument( |
| 180 | + '--message-file', |
| 181 | + dest = 'message_file', |
| 182 | + |
| 183 | + help = |
| 184 | + 'Path to file containing the message to send. ' |
| 185 | + 'If not provided, you will be prompted for the message via stdin.', |
| 186 | + ) |
| 187 | + |
| 188 | + parser.add_argument( |
| 189 | + '--message-encoding', |
| 190 | + dest = 'message_encoding', |
| 191 | + default = 'utf-8', |
| 192 | + |
| 193 | + help = 'Encoding used to interpret message.', |
| 194 | + ) |
| 195 | + |
| 196 | + parser.add_argument( |
| 197 | + '--depth', |
| 198 | + default = 3, |
| 199 | + type = int, |
| 200 | + |
| 201 | + help = 'Depth at which to attach the resulting transactions.', |
| 202 | + ) |
| 203 | + |
| 204 | + parser.add_argument( |
| 205 | + '--min-weight-magnitude', |
| 206 | + dest = 'min_weight_magnitude', |
| 207 | + type = int, |
| 208 | + |
| 209 | + help = |
| 210 | + 'Min weight magnitude, used by the node to calibrate PoW. ' |
| 211 | + 'If not provided, a default value will be used.', |
| 212 | + ) |
| 213 | + |
| 214 | + parser.add_argument( |
| 215 | + '--dry-run', |
| 216 | + action = 'store_true', |
| 217 | + default = False, |
| 218 | + dest = 'dry_run', |
| 219 | + |
| 220 | + help = |
| 221 | + 'If set, resulting transactions will be sent to stdout instead of' |
| 222 | + 'broadcasting to the Tangle.', |
| 223 | + ) |
| 224 | + |
| 225 | + return parser |
| 226 | + |
| 227 | + |
| 228 | +if __name__ == '__main__': |
| 229 | + IotaMamExample().main() |
0 commit comments