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