@@ -15,13 +15,15 @@ import {
15 signTransaction,
15 signTransaction,
16 sendSignedTransaction,
16 sendSignedTransaction,
17 waitForTransactionConfirm,
17 waitForTransactionConfirm,
19} from '../lib/txn'
21} from '../lib/txn'
20import * as tank from '../lib/tank'
22import * as tank from '../lib/tank'
22const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
24const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
23−const GAS_LIMIT = 500000; //TODO fine-tune
25+const GAS_LIMIT = 350000;
24−const INVITE_COST = (GAS_PRICE_GWEI * 1000000000 * GAS_LIMIT);
26+const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
26const STATUS = {
28const STATUS = {
27 INPUT: 'Generate invites',
29 INPUT: 'Generate invites',
@@ -50,13 +52,15 @@ class InvitesSend extends React.Component {
50 status: STATUS.INPUT,
52 status: STATUS.INPUT,
51 errors: Nothing(),
53 errors: Nothing(),
53− invites: []
55+ fundingMessage: Nothing(),
56 this.findInvited = this.findInvited.bind(this);
59 this.findInvited = this.findInvited.bind(this);
57 this.generateInvites = this.generateInvites.bind(this);
60 this.generateInvites = this.generateInvites.bind(this);
58 this.sendInvites = this.sendInvites.bind(this);
61 this.sendInvites = this.sendInvites.bind(this);
59 this.canSend = this.canSend.bind(this);
62 this.canSend = this.canSend.bind(this);
63+ this.addError = this.addError.bind(this);
60 this.handleEmailInput = this.handleEmailInput.bind(this);
64 this.handleEmailInput = this.handleEmailInput.bind(this);
61 this.addTarget = this.addTarget.bind(this);
65 this.addTarget = this.addTarget.bind(this);
62 this.removeTarget = this.removeTarget.bind(this);
66 this.removeTarget = this.removeTarget.bind(this);
@@ -130,26 +134,27 @@ class InvitesSend extends React.Component {
131 const web3 = this.web3;
135 const web3 = this.web3;
132 const targets = this.state.targets;
136 const targets = this.state.targets;
133− console.log('getting planets');
134 const planets = await azimuth.delegatedSending.getPlanetsToSend(
137 const planets = await azimuth.delegatedSending.getPlanetsToSend(
135 this.contracts, this.point, targets.length
138 this.contracts, this.point, targets.length
137− console.log('got planets', planets.length);
140+
141+ // account for the race condition where invites got used up while we were
142+ // composing our target list
138 if (planets.length < targets.length) {
143 if (planets.length < targets.length) {
139− //TODO proper display state
140− throw new Error('can currently only send '+planets.length+' invites...');
144+ this.addError('Can currently only send '+planets.length+' invites!');
143− let invites = []; // { recipient, ticket, signedTx, rawTx }
144− console.log('generating emails...');
145 const nonce = await web3.eth.getTransactionCount(this.address);
148 const nonce = await web3.eth.getTransactionCount(this.address);
146 const chainId = await web3.eth.net.getId();
149 const chainId = await web3.eth.net.getId();
147− console.log('chainId', chainId);
150+
151+ // create invite wallets and transactions
152+ let invites = []; // { email, ticket, signedTx, rawTx }
148 for (let i = 0; i < targets.length; i++) {
153 for (let i = 0; i < targets.length; i++) {
149 console.log('generating email', i, targets[i].email);
154 console.log('generating email', i, targets[i].email);
150 this.setEmailStatus(i, EMAIL_STATUS.BUSY);
155 this.setEmailStatus(i, EMAIL_STATUS.BUSY);
152− const recipient = targets[i].email;
157+ const email = targets[i].email;
153 const wallet = await this.getTemporaryWallet();
158 const wallet = await this.getTemporaryWallet();
155 let inviteTx = azimuth.delegatedSending.sendPoint(
160 let inviteTx = azimuth.delegatedSending.sendPoint(
@@ -166,15 +171,15 @@ class InvitesSend extends React.Component {
167 const rawTx = hexify(signedTx.serialize());
172 const rawTx = hexify(signedTx.serialize());
169− invites.push({recipient, ticket: wallet.ticket, signedTx, rawTx });
174+ invites.push({email, ticket: wallet.ticket, signedTx, rawTx });
170 this.setEmailStatus(i, EMAIL_STATUS.DONE);
175 this.setEmailStatus(i, EMAIL_STATUS.DONE);
173− this.setState({ invites, status: STATUS.CAN_SEND });
178+ this.setState({ wipInvites: invites, status: STATUS.CAN_SEND });
176 async sendInvites() {
181 async sendInvites() {
177− const invites = this.state.invites;
182+ const invites = this.state.wipInvites;
178 const web3 = this.web3;
183 const web3 = this.web3;
180 //TODO invite status isn't changing... why?
185 //TODO invite status isn't changing... why?
@@ -188,7 +193,7 @@ class InvitesSend extends React.Component {
188 (INVITE_COST * invites.length),
193 (INVITE_COST * invites.length),
189 invites.map(i => i.rawTx), this.askForFunding
194 invites.map(i => i.rawTx), this.askForFunding
191− this.setState({ message: Nothing() }); //TODO see /lib/tank waitForBalance
196+ this.setState({ fundingMessage: Nothing() }); //TODO see /lib/tank waitForBalance
193 this.setState({ status: STATUS.SENDING });
198 this.setState({ status: STATUS.SENDING });
@@ -200,14 +205,14 @@ class InvitesSend extends React.Component {
200 web3, Just(invite.signedTx), tankWasUsed, ()=>{}
205 web3, Just(invite.signedTx), tankWasUsed, ()=>{}
201 ).catch(err => {
206 ).catch(err => {
202 console.error('invite tx sending failed', err);
207 console.error('invite tx sending failed', err);
203− this.addError('Invite transaction not sent for ' + invite.recipient);
208+ this.addError('Invite transaction not sent for ' + invite.email);
204 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
209 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
207 // ask the email sender to send this. we can do this prior to tx confirm,
212 // ask the email sender to send this. we can do this prior to tx confirm,
208 // because the sender service waits for confirm & success for us.
213 // because the sender service waits for confirm & success for us.
209 const mailSuccess = await sendMail(
214 const mailSuccess = await sendMail(
210− invite.recipient, invite.ticket, invite.rawTx
215+ invite.email, invite.ticket, invite.rawTx
212 //TODO but this doesn't catch sender-side failures... we may
217 //TODO but this doesn't catch sender-side failures... we may
213 // just need really good monitoring for that...
218 // just need really good monitoring for that...
@@ -218,7 +223,7 @@ class InvitesSend extends React.Component {
218 //NOTE this assumes that the transaction succeeds, but we don't know
223 //NOTE this assumes that the transaction succeeds, but we don't know
219 // that for a fact yet...
224 // that for a fact yet...
220 this.addError(
225 this.addError(
221− 'Invite email failed to send for ' + invite.recipient +
226+ 'Invite email failed to send for ' + invite.email +
222 '. Please give them this ticket: ' + invite.ticket
227 '. Please give them this ticket: ' + invite.ticket
224 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
229 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
@@ -229,7 +234,7 @@ class InvitesSend extends React.Component {
229 .then(async success => {
234 .then(async success => {
230 if (!success) {
235 if (!success) {
231 console.log('invite tx rejected');
236 console.log('invite tx rejected');
232− this.addError('Invite transaction failed for ' + invite.recipient);
237+ this.addError('Invite transaction failed for ' + invite.email);
233 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
238 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
@@ -248,15 +253,16 @@ class InvitesSend extends React.Component {
248 addError(error) {
253 addError(error) {
249 const newError = this.state.errors.matchWith({
254 const newError = this.state.errors.matchWith({
250 Nothing: () => [error],
255 Nothing: () => [error],
251− Just: errs => errs.value.push(error)
256+ Just: errs => { errs.value.push(error); return errs.value; }
253 this.setState({ errors: Just(newError) });
258 this.setState({ errors: Just(newError) });
256 askForFunding(address, minBalance, balance) {
261 askForFunding(address, minBalance, balance) {
257− this.setState({ message: Just(
262+ this.setState({ fundingMessage: Just(
258− `Please make sure ${address} has at least ${minBalance} wei,` +
263+ `Please make sure ${address} has at least ${fromWei(minBalance)} ETH,` +
259− `we'll continue once that's true. Current balance: ${balance}. Waiting...`
264+ `we'll continue once that's true. Current balance: ${fromWei(balance)}.` +
@@ -264,6 +270,7 @@ class InvitesSend extends React.Component {
264 let targets = this.state.targets;
270 let targets = this.state.targets;
265 targets[i].email = email;
271 targets[i].email = email;
273+ // if it's a valid address, check if it has received an invite before
267 if (email.match(/.*@.*\...+/)) {
274 if (email.match(/.*@.*\...+/)) {
268 hasReceived(email)
275 hasReceived(email)
269 .then(res => {
276 .then(res => {
@@ -273,14 +280,18 @@ class InvitesSend extends React.Component {
274 targets[i].hasReceived = Nothing();
281 targets[i].hasReceived = Nothing();
276 this.setState({ targets });
284 this.setState({ targets });
279 canSend() {
287 canSend() {
280 const targets = this.state.targets;
288 const targets = this.state.targets;
281 // may not have duplicate targets
290 // may not have duplicate targets
282 if ((new Set(targets.map(t => t.email))).size !== targets.length)
291 if ((new Set(targets.map(t => t.email))).size !== targets.length)
283 return false;
292 return false;
294+ // none may have received invites already
284 for (let i = 0; i < targets.length; i++) {
295 for (let i = 0; i < targets.length; i++) {
285 const target = targets[i];
296 const target = targets[i];
286 const res = target.hasReceived.matchWith({
297 const res = target.hasReceived.matchWith({
@@ -289,6 +300,7 @@ class InvitesSend extends React.Component {
290 if (res) return false;
301 if (res) return false;
292 return this.state.invitesAvailable.matchWith({
304 return this.state.invitesAvailable.matchWith({
293 Nothing: () => false,
305 Nothing: () => false,
294 Just: invites => (invites.value > 0)
306 Just: invites => (invites.value > 0)
@@ -328,7 +340,7 @@ class InvitesSend extends React.Component {
331− let error = this.state.errors.matchWith({
343+ const error = this.state.errors.matchWith({
332 Nothing: () => null,
344 Nothing: () => null,
333 Just: (errors) => (<Warning className={'mt-8'}>
345 Just: (errors) => (<Warning className={'mt-8'}>
334 <H3 style={{marginTop: 0, paddingTop: 0}}>
346 <H3 style={{marginTop: 0, paddingTop: 0}}>
@@ -338,6 +350,11 @@ class InvitesSend extends React.Component {
338 </Warning>)
350 </Warning>)
353+ const fundingMessage = this.state.fundingMessage.matchWith({
354+ Nothing: () => null,
355+ Just: msg => (<Warning className={'mt-8'}>{msg.value}</Warning>)
341 //TODO don't render inputs at all if STATUS.DONE
358 //TODO don't render inputs at all if STATUS.DONE
343 const inputDisabled = (this.state.status !== STATUS.INPUT);
360 const inputDisabled = (this.state.status !== STATUS.INPUT);
@@ -378,6 +395,8 @@ class InvitesSend extends React.Component {
379 { error }
396 { error }
382 prop-size={'s narrow'}
401 prop-size={'s narrow'}
383 className={'mt-8'}
402 className={'mt-8'}