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