Skip to content
This repository was archived by the owner on Jan 13, 2023. It is now read-only.

Commit f9631ea

Browse files
committed
[#47] Proof of concept to integrate JS MAM into Python.
1 parent b3a2fac commit f9631ea

File tree

3 files changed

+240
-10
lines changed

3 files changed

+240
-10
lines changed

examples/mam_js_send.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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()

src/iota/bin/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ class IotaCommandLineApp(with_metaclass(ABCMeta)):
2929
Whether the command requires the user to provide a seed.
3030
"""
3131

32-
def __init__(self, stdout=sys.stdout, stderr=sys.stderr):
33-
# type: (StringIO, StringIO) -> None
32+
def __init__(self, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin):
33+
# type: (StringIO, StringIO, StringIO) -> None
3434
super(IotaCommandLineApp, self).__init__()
3535

3636
self.stdout = stdout
3737
self.stderr = stderr
38+
self.stdin = stdin
3839

3940
@abstract_method
4041
def execute(self, api, **arguments):
@@ -113,7 +114,7 @@ def create_argument_parser(self):
113114
arguments and options from argv.
114115
"""
115116
parser = ArgumentParser(
116-
description = __doc__,
117+
description = self.__doc__,
117118
epilog = 'PyOTA v{version}'.format(version=__version__),
118119
)
119120

@@ -139,6 +140,13 @@ def create_argument_parser(self):
139140
'via stdin.',
140141
)
141142

143+
parser.add_argument(
144+
'--testnet',
145+
action = 'store_true',
146+
default = False,
147+
help = 'If set, use testnet settings (e.g., for PoW).',
148+
)
149+
142150
return parser
143151

144152
@staticmethod

src/iota/bin/repl.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,6 @@ def create_argument_parser(self):
7272
help = 'URI of node to send POW requests to.'
7373
)
7474

75-
parser.add_argument(
76-
'--testnet',
77-
action = 'store_true',
78-
default = False,
79-
help = 'If set, use testnet settings (e.g., for PoW).',
80-
)
81-
8275
parser.add_argument(
8376
'--debug',
8477
action = 'store_true',

0 commit comments

Comments
 (0)