@defdone/annon 0.1.0-alpha.0 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/CLI.md +116 -10
  3. package/README.md +57 -15
  4. package/dist/api/annon-types.d.ts +2 -1
  5. package/dist/api/annon.js +9 -2
  6. package/dist/browser/index.d.ts +1 -1
  7. package/dist/browser/login-bundle.js +113 -77
  8. package/dist/browser/login-dom.d.ts +13 -0
  9. package/dist/browser/login-dom.js +67 -0
  10. package/dist/browser/login-dropdown.d.ts +32 -0
  11. package/dist/browser/login-dropdown.js +129 -0
  12. package/dist/browser/login-format.d.ts +16 -0
  13. package/dist/browser/login-format.js +52 -0
  14. package/dist/browser/login-status.d.ts +70 -0
  15. package/dist/browser/login-status.js +85 -0
  16. package/dist/browser/login-terms.d.ts +10 -0
  17. package/dist/browser/login-terms.js +69 -0
  18. package/dist/browser/login-view.d.ts +34 -0
  19. package/dist/browser/login-view.js +227 -0
  20. package/dist/browser/login-wallet.d.ts +28 -0
  21. package/dist/browser/login-wallet.js +179 -0
  22. package/dist/browser/login.js +115 -101
  23. package/dist/browser/purchase-bundle.js +45 -45
  24. package/dist/browser/purchases.d.ts +4 -0
  25. package/dist/browser/purchases.js +14 -0
  26. package/dist/browser/reconcile.d.ts +41 -0
  27. package/dist/browser/reconcile.js +77 -0
  28. package/dist/node/adapter.js +5 -3
  29. package/dist/node/approval-status.d.ts +34 -0
  30. package/dist/node/approval-status.js +71 -0
  31. package/dist/node/backup-secret-init.d.ts +9 -0
  32. package/dist/node/backup-secret-init.js +103 -0
  33. package/dist/node/backup-secret.d.ts +35 -0
  34. package/dist/node/backup-secret.js +104 -0
  35. package/dist/node/browser-login.d.ts +9 -0
  36. package/dist/node/browser-login.js +75 -14
  37. package/dist/node/buy-commands.js +9 -2
  38. package/dist/node/cli.js +23 -7
  39. package/dist/node/command-context.js +1 -1
  40. package/dist/node/config.d.ts +2 -0
  41. package/dist/node/config.js +3 -1
  42. package/dist/node/login-page.d.ts +7 -0
  43. package/dist/node/login-page.js +71 -0
  44. package/dist/node/purchase-commands.d.ts +2 -2
  45. package/dist/node/purchase-commands.js +10 -6
  46. package/dist/node/saved-state.d.ts +4 -0
  47. package/dist/node/saved-state.js +50 -0
  48. package/dist/node/signer-wallet.js +3 -1
  49. package/dist/node/version.d.ts +1 -0
  50. package/dist/node/version.js +3 -0
  51. package/dist/node/wallets.d.ts +1 -1
  52. package/dist/node/wallets.js +2 -10
  53. package/dist/pay/base.js +36 -43
  54. package/dist/pay/purchase.d.ts +2 -1
  55. package/dist/wallet/account-only.d.ts +1 -1
  56. package/dist/wallet/account-only.js +1 -1
  57. package/dist/wallet/uncertain.d.ts +6 -3
  58. package/dist/wallet/uncertain.js +4 -1
  59. package/package.json +3 -2
@@ -1,13 +1,14 @@
1
- import { setupWalletSelector } from '@near-wallet-selector/core';
2
- import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet';
3
1
  import { Buffer } from 'buffer';
4
- import { signInAccountOnly } from '../wallet/account-only.js';
5
- import { evmWallet } from '../wallet/wallet.js';
2
+ import { pageElements, renderSummary, showStatus } from './login-dom.js';
3
+ import { walletPicker } from './login-dropdown.js';
4
+ import { expiryText } from './login-format.js';
5
+ import { connected, SendUncertain, STATUS, walletFailure } from './login-status.js';
6
+ import { describeRequest, technicalDetails } from './login-view.js';
7
+ import { pageWallets } from './login-wallet.js';
6
8
  Object.assign(globalThis, { Buffer });
7
- const pair = document.getElementById('pair');
8
- const display = document.getElementById('action');
9
- const status = document.getElementById('status');
10
- const approve = document.getElementById('approve');
9
+ const el = pageElements(document);
10
+ const approve = el.approve;
11
+ const cancel = document.getElementById('cancel');
11
12
  const fragment = new URLSearchParams(location.hash.slice(1));
12
13
  const redirected = location.pathname === '/callback';
13
14
  let capability = redirected ? '' : location.hash.slice(1);
@@ -25,9 +26,51 @@ const api = async (path, body) => {
25
26
  throw new Error('Session expired or wallet result rejected. Return to the terminal.');
26
27
  return r.json();
27
28
  };
28
- const used = new Set();
29
+ /** Requests whose result went to the CLI, or that ended here: never signed again. */
30
+ const done = new Set();
29
31
  const callbacks = new Map();
32
+ const wallets = pageWallets({
33
+ expect: (state, id) => callbacks.set(state, id),
34
+ forget: (state) => callbacks.delete(state),
35
+ sending: async (id) => {
36
+ // The wallet may broadcast from now on: this request is never attempted again here, and the
37
+ // CLI records it before the wallet is called.
38
+ done.add(id);
39
+ await api('/api/report', { id, reason: 'send_started' });
40
+ },
41
+ });
30
42
  let current;
43
+ let expiry;
44
+ let busy = false;
45
+ const status = (s) => showStatus(el, s);
46
+ const picker = walletPicker(document, () => controls());
47
+ const walletId = () => picker.value();
48
+ /** Buttons and wallet choice follow the request: usable only while it is open and idle. */
49
+ function controls() {
50
+ const q = current;
51
+ const open = !!q && !done.has(q.id) && Date.now() < q.expiresAt;
52
+ approve.disabled = !open || busy;
53
+ if (cancel) {
54
+ cancel.hidden = !open;
55
+ cancel.disabled = busy;
56
+ }
57
+ // No wallet switch while a wallet is open or once the request is finished or sent.
58
+ picker.setLocked(!open || busy);
59
+ approve.textContent =
60
+ q && wallets.needsConnect(q, walletId()) ? 'Connect wallet' : 'Review in wallet';
61
+ }
62
+ async function submit(id, result) {
63
+ done.add(id);
64
+ status(STATUS.verifying);
65
+ controls();
66
+ try {
67
+ await api('/api/result', { id, result });
68
+ status(STATUS.signed);
69
+ }
70
+ catch {
71
+ status(STATUS.rejected);
72
+ }
73
+ }
31
74
  if (redirected) {
32
75
  // Only a per-action opaque nonce leaves this origin. The capability stays in the opener.
33
76
  window.opener?.postMessage({
@@ -39,7 +82,7 @@ if (redirected) {
39
82
  signature: fragment.get('signature'),
40
83
  },
41
84
  }, location.origin);
42
- status.textContent = 'Proof returned to the approval window. Return to your terminal.';
85
+ status(STATUS.redirected);
43
86
  }
44
87
  else {
45
88
  window.addEventListener('message', (event) => {
@@ -49,129 +92,100 @@ else {
49
92
  if (!id)
50
93
  return;
51
94
  callbacks.delete(event.data.state);
52
- void api('/api/result', { id, result: event.data.result })
53
- .then(() => {
54
- status.textContent = 'Proof verified. Return to your terminal.';
55
- })
56
- .catch((e) => {
57
- status.textContent = e.message;
58
- });
95
+ void submit(id, event.data.result);
59
96
  });
60
97
  }
98
+ /** Shows the CLI's current request; a finished one stays on screen with its outcome. */
99
+ function show(q, sendStarted = false) {
100
+ if (q && q.id !== current?.id) {
101
+ current = q;
102
+ expiry = renderSummary(document, el, describeRequest(q), technicalDetails(q));
103
+ el['wallet-field'].hidden = !q.operation.startsWith('near.');
104
+ picker.setDisabled('hot-wallet', q.chain !== 'near:mainnet');
105
+ status(wallets.needsConnect(q, walletId()) ? STATUS.connect : STATUS.ready);
106
+ }
107
+ const last = current;
108
+ // A send started from an earlier load of this page: the wallet may have broadcast it.
109
+ if (last && q && sendStarted && !done.has(last.id)) {
110
+ done.add(last.id);
111
+ status({ tone: 'error', text: walletFailure(new SendUncertain()).text });
112
+ }
113
+ if (!last || !expiry) {
114
+ status(STATUS.waitingCli);
115
+ }
116
+ else {
117
+ expiry.textContent = expiryText(last.expiresAt, Date.now());
118
+ const expired = Date.now() >= last.expiresAt;
119
+ // The CLI dropped the request (expired, cancelled or session over) before a result was sent.
120
+ if (!done.has(last.id) && (!q || expired)) {
121
+ done.add(last.id);
122
+ status(expired ? STATUS.expired : STATUS.ended);
123
+ }
124
+ }
125
+ controls();
126
+ }
61
127
  async function poll() {
62
128
  if (!capability) {
63
- status.textContent = 'Open the exact URL printed by your CLI.';
129
+ status(STATUS.noCapability);
64
130
  return;
65
131
  }
66
132
  try {
67
133
  const r = await api('/api/action');
68
- pair.textContent = `Pairing code: ${r.pairingCode}. Match this with your terminal.`;
69
- current = r.action ?? undefined;
70
- display.textContent = current ? JSON.stringify(current, null, 2) : 'Waiting for the CLI…';
71
- approve.disabled = !current || used.has(current.id);
134
+ el.pair.textContent = String(r.pairingCode);
135
+ show(r.action ?? undefined, r.sendStarted === true);
72
136
  setTimeout(() => {
73
137
  void poll();
74
138
  }, 1000);
75
139
  }
76
140
  catch (e) {
77
- status.textContent = e.message;
141
+ status({ tone: 'error', text: e.message });
78
142
  capability = '';
79
143
  approve.disabled = true;
80
144
  }
81
145
  }
82
146
  approve.onclick = async () => {
83
147
  const q = current;
84
- if (!q || used.has(q.id) || Date.now() >= q.expiresAt)
148
+ if (!q || busy || done.has(q.id) || Date.now() >= q.expiresAt)
85
149
  return;
86
- used.add(q.id);
87
- approve.disabled = true;
150
+ busy = true;
151
+ controls();
152
+ status(STATUS.wallet);
88
153
  try {
89
- let result;
90
- if (q.operation.startsWith('evm.')) {
91
- const provider = window.ethereum;
92
- if (!provider)
93
- throw new Error('Install an injected EVM wallet.');
94
- const accounts = (await provider.request({ method: 'eth_requestAccounts' }));
95
- const address = q.account || accounts[0];
96
- if (!address)
97
- throw new Error('No account');
98
- const chain = await provider.request({ method: 'eth_chainId' });
99
- if (`eip155:${BigInt(String(chain))}` !== q.chain)
100
- throw new Error(`Select ${q.chain} in your wallet.`);
101
- const wallet = evmWallet(provider, address.toLowerCase());
102
- result =
103
- q.operation === 'evm.personalSign'
104
- ? {
105
- signature: (await wallet.authorize(q.payload.message))?.sig ?? '',
106
- account: wallet.identity.id,
107
- }
108
- : { signature: await wallet.signTypedData(q.payload) };
109
- }
110
- else {
111
- const networkId = q.chain === 'near:mainnet' ? 'mainnet' : 'testnet';
112
- const selector = await setupWalletSelector({
113
- network: networkId,
114
- modules: [setupMyNearWallet()],
115
- });
116
- const wallet = await selector.wallet('my-near-wallet');
117
- const accounts = await signInAccountOnly(wallet);
118
- const account = q.account || accounts[0]?.accountId;
119
- if (!account || !accounts.some((a) => a.accountId === account))
120
- throw new Error('Select the requested NEAR account.');
121
- if (q.operation === 'near.nep413') {
122
- if (!wallet.signMessage)
123
- throw new Error('NEP-413 unavailable');
124
- const p = q.payload;
125
- const state = crypto.randomUUID();
126
- callbacks.set(state, q.id);
127
- const signed = await wallet.signMessage({
128
- ...p,
129
- nonce: Buffer.from(p.nonce),
130
- state,
131
- });
132
- if (!signed)
133
- return; // redirect wallet: /callback forwards fragment
134
- callbacks.delete(state);
135
- result = signed;
136
- }
137
- else {
138
- const p = q.payload;
139
- const outcome = await wallet.signAndSendTransaction({
140
- signerId: account,
141
- receiverId: p.receiverId,
142
- // Selector v10 passes Borsh Action structs through to its transaction encoder.
143
- actions: [
144
- {
145
- functionCall: {
146
- methodName: p.method,
147
- args: Buffer.from(JSON.stringify(p.args)),
148
- gas: BigInt(p.gas),
149
- deposit: BigInt(p.deposit),
150
- },
151
- },
152
- ],
153
- });
154
- if (!outcome)
155
- return;
156
- result = { txHash: outcome.transaction.hash };
157
- }
158
- }
159
- await api('/api/result', { id: q.id, result });
160
- status.textContent = 'Verified. Return to the terminal.';
154
+ const outcome = await wallets.run(q, walletId());
155
+ busy = false;
156
+ if (outcome.kind === 'connected')
157
+ status(connected(outcome.account));
158
+ else if (outcome.kind === 'result')
159
+ await submit(q.id, outcome.result);
161
160
  }
162
161
  catch (e) {
163
- status.textContent = e.message;
162
+ // One click per attempt, never retried here: the person may try this request again until it
163
+ // expires. The CLI hears only a fixed reason name.
164
+ const failure = walletFailure(e);
165
+ status({ tone: 'error', text: failure.text });
166
+ void api('/api/report', { id: q.id, reason: failure.reason }).catch(() => { });
164
167
  }
165
168
  finally {
166
- approve.disabled = true;
169
+ busy = false;
170
+ controls();
167
171
  }
168
172
  };
173
+ if (cancel)
174
+ cancel.onclick = () => {
175
+ const q = current;
176
+ if (!q || busy || done.has(q.id))
177
+ return;
178
+ done.add(q.id);
179
+ controls();
180
+ status({ tone: 'error', text: 'Cancelled. Return to the terminal.' });
181
+ void api('/api/report', { id: q.id, reason: 'cancelled' }).catch(() => { });
182
+ };
169
183
  if (!redirected) {
170
184
  void (async () => {
171
185
  if (!capability)
172
186
  capability = (await api('/api/bootstrap')).capability;
173
187
  await poll();
174
188
  })().catch((e) => {
175
- status.textContent = e.message;
189
+ status({ tone: 'error', text: e.message });
176
190
  });
177
191
  }