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