on e7ae14d help sign in
bridge/src/lib/invite.js
271 lines 7.2 kB history raw
1import Tx from 'ethereumjs-tx';
2import * as azimuth from 'azimuth-js';
3import * as wg from './walletgen';
4import * as tank from './tank';
6import JSZip from 'jszip';
7import saveAs from 'file-saver';
9import { hexify } from './txn';
10import { deriveNetworkSeedFromUrbitWallet, deriveNetworkKeys } from './keys';
11import { addHexPrefix } from './wallet';
12import { sendAndAwaitAll } from './txn';
13import { GAS_LIMITS } from './constants';
15// the initial network key revision is always 1
16const INITIAL_NETWORK_KEY_REVISION = 1;
18const TRANSACTION_STATES = {
19 GENERATING: {
20 label: 'Generating Transactions...',
21 progress: 0.0,
22 },
23 SIGNING: {
24 label: 'Signing Transactions...',
25 progress: 0.15,
26 },
27 FUNDING: {
28 label: 'Funding Transactions...',
29 progress: 0.3,
30 },
31 TRANSFERRING: {
32 label: 'Transferring Point...',
33 progress: 0.55,
34 },
35 CLEANING: {
36 label: 'Cleaning Up...',
37 progress: 0.95,
38 },
39 DONE: {
40 label: 'Done',
41 progress: 1.0,
42 },
43};
45export async function generateWallet(point) {
46 const ticket = await wg.makeTicket(point);
47 const wallet = await wg.generateWallet(point, ticket);
48 return wallet;
51//TODO should be moved to lib/walletgen
52export async function downloadWallet(paper) {
53 return new Promise((resolve, reject) => {
54 const zip = new JSZip();
55 //TODO the categories here aren't explained in bridge at all...
57 const bin0 = paper.filter(item => item.bin === '0');
58 const bin1 = paper.filter(item => item.bin === '1');
59 const bin2 = paper.filter(item => item.bin === '2');
60 const bin3 = paper.filter(item => item.bin === '3');
61 const bin4 = paper.filter(item => item.bin === '4');
63 const bin0Folder = zip.folder('0. Public');
64 const bin1Folder = zip.folder('1. Very High Friction Custody');
65 const bin2Folder = zip.folder('2. High Friction Custody');
66 const bin3Folder = zip.folder('3. Medium Friction Custody');
67 const bin4Folder = zip.folder('4. Low Friction Custody');
69 bin0.forEach(item => bin0Folder.file(`${item.pageTitle}.png`, item.png));
70 bin1.forEach(item => bin1Folder.file(`${item.pageTitle}.png`, item.png));
71 bin2.forEach(item => bin2Folder.file(`${item.pageTitle}.png`, item.png));
72 bin3.forEach(item => bin3Folder.file(`${item.pageTitle}.png`, item.png));
73 bin4.forEach(item => bin4Folder.file(`${item.pageTitle}.png`, item.png));
75 zip.generateAsync({ type: 'blob' }).then(content => {
76 saveAs(content, 'urbit-wallet.zip');
77 resolve(true);
78 });
79 });
82export async function reticketPointBetweenWallets({
83 fromWallet,
84 toWallet,
85 point,
86 web3,
87 contracts,
88 onUpdate,
89 transferEth = false,
90}) {
91 const askForFunding = (address, minBalance, balance) =>
92 onUpdate({
93 type: 'askFunding',
94 value: { address, minBalance, balance },
95 });
97 const gotFunding = () => onUpdate({ type: 'gotFunding' });
98 const progress = state => onUpdate({ type: 'progress', state });
100 //
101 // generate transactions
102 //
104 progress(TRANSACTION_STATES.GENERATING);
106 // transfer to invite wallet, so it can configure
107 //NOTE doing this in the invite wallet instead of the final wallet makes
108 // failure cases "solvable" by just telling the user to fully retry.
109 // in other words, the invite flow is effectively idempotent.
111 //TODO no harm done if we already owned it, but should still get a bool arg
112 // for skipping this, if it isn't too big a burden for callers
113 const transferTmpTx = azimuth.ecliptic.transferPoint(
114 contracts,
115 point,
116 fromWallet.address,
117 false
118 );
119 transferTmpTx.gas = GAS_LIMITS.TRANSFER;
121 // configure networking public keys
122 const seed = await deriveNetworkSeedFromUrbitWallet(
123 toWallet,
124 INITIAL_NETWORK_KEY_REVISION
125 );
127 const networkSeed = seed.matchWith({
128 Nothing: () => {
129 throw new Error('network seed not derived');
130 },
131 Just: p => p.value,
132 });
134 const networkKeys = deriveNetworkKeys(networkSeed);
136 const keysTx = azimuth.ecliptic.configureKeys(
137 contracts,
138 point,
139 addHexPrefix(networkKeys.crypt.public),
140 addHexPrefix(networkKeys.auth.public),
141 1,
142 false
143 );
144 keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
146 // configure management proxy
148 const managementTx = azimuth.ecliptic.setManagementProxy(
149 contracts,
150 point,
151 toWallet.management.keys.address
152 );
153 managementTx.gas = GAS_LIMITS.SET_PROXY;
155 // set spawn & voting proxies situationally
157 let txs = [transferTmpTx, keysTx, managementTx];
158 if (
159 azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
160 ) {
161 const spawnTx = azimuth.ecliptic.setSpawnProxy(
162 contracts,
163 point,
164 toWallet.spawn.keys.address
165 );
166 spawnTx.gas = GAS_LIMITS.SET_PROXY;
167 txs.push(spawnTx);
169 if (
170 azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
171 ) {
172 const votingTx = azimuth.ecliptic.setVotingProxy(
173 contracts,
174 point,
175 toWallet.voting.keys.address
176 );
177 votingTx.gas = GAS_LIMITS.SET_PROXY;
178 txs.push(votingTx);
179 }
180 }
182 // transfer configured point to user's new wallet
184 const transferFinalTx = azimuth.ecliptic.transferPoint(
185 contracts,
186 point,
187 toWallet.ownership.keys.address,
188 false
189 );
190 transferFinalTx.gas = GAS_LIMITS.TRANSFER;
191 txs.push(transferFinalTx);
193 //
194 // finalizing & signing transactions
195 //
197 progress(TRANSACTION_STATES.SIGNING);
199 let totalCost = 0;
200 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
201 txs = txs.map(tx => {
202 tx.from = fromWallet.address;
203 tx.nonce = inviteNonce++;
204 tx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
205 totalCost = totalCost + tx.gasPrice * tx.gas;
206 return tx;
207 });
209 let txPairs = txs.map(tx => {
210 let stx = new Tx(tx);
211 stx.sign(Buffer.from(fromWallet.privateKey, 'hex'));
212 return {
213 raw: hexify(stx.serialize()),
214 signed: stx,
215 };
216 });
218 //
219 // ensuring funding for transactions
220 //
222 progress(TRANSACTION_STATES.FUNDING);
224 const usedTank = await tank.ensureFundsFor(
225 web3,
226 point,
227 fromWallet.address,
228 totalCost,
229 txPairs.map(p => p.raw),
230 askForFunding,
231 gotFunding
232 );
234 //
235 // sending and awaiting transactions
236 //
238 progress(TRANSACTION_STATES.TRANSFERRING);
240 await sendAndAwaitAll(web3, txPairs.map(p => p.signed), usedTank);
242 //
243 // move leftover eth
244 //
246 progress(TRANSACTION_STATES.CLEANING);
248 // if non-trivial eth left in invite wallet, transfer to new ownership
249 let balance = await web3.eth.getBalance(fromWallet.address);
250 const gasPrice = 20000000000;
251 const gasLimit = 21000;
252 const sendEthCost = gasPrice * gasLimit;
253 if (transferEth && balance > sendEthCost) {
254 const value = balance - sendEthCost;
255 const tx = {
256 to: toWallet.ownership.keys.address,
257 value: value,
258 gasPrice: gasPrice,
259 gas: gasLimit,
260 nonce: inviteNonce++,
261 };
262 let stx = new Tx(tx);
263 stx.sign(fromWallet.privateKey);
264 const rawTx = hexify(stx.serialize());
265 web3.eth.sendSignedTransaction(rawTx).catch(err => {
266 console.log('error sending value tx, who cares', err);
267 });
268 }
270 progress(TRANSACTION_STATES.DONE);