@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.
- package/CHANGELOG.md +15 -0
- package/CLI.md +116 -10
- package/README.md +57 -15
- package/dist/api/annon-types.d.ts +2 -1
- package/dist/api/annon.js +9 -2
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/login-bundle.js +113 -77
- package/dist/browser/login-dom.d.ts +13 -0
- package/dist/browser/login-dom.js +67 -0
- package/dist/browser/login-dropdown.d.ts +32 -0
- package/dist/browser/login-dropdown.js +129 -0
- package/dist/browser/login-format.d.ts +16 -0
- package/dist/browser/login-format.js +52 -0
- package/dist/browser/login-status.d.ts +70 -0
- package/dist/browser/login-status.js +85 -0
- package/dist/browser/login-terms.d.ts +10 -0
- package/dist/browser/login-terms.js +69 -0
- package/dist/browser/login-view.d.ts +34 -0
- package/dist/browser/login-view.js +227 -0
- package/dist/browser/login-wallet.d.ts +28 -0
- package/dist/browser/login-wallet.js +179 -0
- package/dist/browser/login.js +115 -101
- package/dist/browser/purchase-bundle.js +45 -45
- package/dist/browser/purchases.d.ts +4 -0
- package/dist/browser/purchases.js +14 -0
- package/dist/browser/reconcile.d.ts +41 -0
- package/dist/browser/reconcile.js +77 -0
- package/dist/node/adapter.js +5 -3
- package/dist/node/approval-status.d.ts +34 -0
- package/dist/node/approval-status.js +71 -0
- package/dist/node/backup-secret-init.d.ts +9 -0
- package/dist/node/backup-secret-init.js +103 -0
- package/dist/node/backup-secret.d.ts +35 -0
- package/dist/node/backup-secret.js +104 -0
- package/dist/node/browser-login.d.ts +9 -0
- package/dist/node/browser-login.js +75 -14
- package/dist/node/buy-commands.js +9 -2
- package/dist/node/cli.js +23 -7
- package/dist/node/command-context.js +1 -1
- package/dist/node/config.d.ts +2 -0
- package/dist/node/config.js +3 -1
- package/dist/node/login-page.d.ts +7 -0
- package/dist/node/login-page.js +71 -0
- package/dist/node/purchase-commands.d.ts +2 -2
- package/dist/node/purchase-commands.js +10 -6
- package/dist/node/saved-state.d.ts +4 -0
- package/dist/node/saved-state.js +50 -0
- package/dist/node/signer-wallet.js +3 -1
- package/dist/node/version.d.ts +1 -0
- package/dist/node/version.js +3 -0
- package/dist/node/wallets.d.ts +1 -1
- package/dist/node/wallets.js +2 -10
- package/dist/pay/base.js +36 -43
- package/dist/pay/purchase.d.ts +2 -1
- package/dist/wallet/account-only.d.ts +1 -1
- package/dist/wallet/account-only.js +1 -1
- package/dist/wallet/uncertain.d.ts +6 -3
- package/dist/wallet/uncertain.js +4 -1
- package/package.json +3 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Puts a request summary and status on the approval page. Every value is set with `textContent`;
|
|
3
|
+
* this module never parses markup.
|
|
4
|
+
*/
|
|
5
|
+
import type { Status } from './login-status.js';
|
|
6
|
+
import type { Summary } from './login-view.js';
|
|
7
|
+
declare const IDS: readonly ["title", "pair", "summary", "amount", "rows", "message-box", "message-label", "message", "terms-box", "terms", "warnings", "note", "wallet-field", "approve", "status", "action"];
|
|
8
|
+
export type PageElements = Record<(typeof IDS)[number], HTMLElement>;
|
|
9
|
+
export declare function pageElements(doc: Document): PageElements;
|
|
10
|
+
/** Shows one request. Returns the element the caller updates with the time left. */
|
|
11
|
+
export declare function renderSummary(doc: Document, el: PageElements, s: Summary, technical: string): HTMLElement;
|
|
12
|
+
export declare function showStatus(el: PageElements, status: Status): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Puts a request summary and status on the approval page. Every value is set with `textContent`;
|
|
3
|
+
* this module never parses markup.
|
|
4
|
+
*/
|
|
5
|
+
const IDS = [
|
|
6
|
+
'title',
|
|
7
|
+
'pair',
|
|
8
|
+
'summary',
|
|
9
|
+
'amount',
|
|
10
|
+
'rows',
|
|
11
|
+
'message-box',
|
|
12
|
+
'message-label',
|
|
13
|
+
'message',
|
|
14
|
+
'terms-box',
|
|
15
|
+
'terms',
|
|
16
|
+
'warnings',
|
|
17
|
+
'note',
|
|
18
|
+
'wallet-field',
|
|
19
|
+
'approve',
|
|
20
|
+
'status',
|
|
21
|
+
'action',
|
|
22
|
+
];
|
|
23
|
+
export function pageElements(doc) {
|
|
24
|
+
return Object.fromEntries(IDS.map((id) => [id, doc.getElementById(id)]));
|
|
25
|
+
}
|
|
26
|
+
function rows(doc, target, list) {
|
|
27
|
+
const values = list.map((r) => {
|
|
28
|
+
const row = doc.createElement('div');
|
|
29
|
+
const dt = doc.createElement('dt');
|
|
30
|
+
const dd = doc.createElement('dd');
|
|
31
|
+
row.className = 'row';
|
|
32
|
+
dt.textContent = r.label;
|
|
33
|
+
dd.textContent = r.value;
|
|
34
|
+
if (r.mono)
|
|
35
|
+
dd.className = 'mono';
|
|
36
|
+
row.append(dt, dd);
|
|
37
|
+
return [row, dd];
|
|
38
|
+
});
|
|
39
|
+
target.replaceChildren(...values.map(([row]) => row));
|
|
40
|
+
return values.map(([, dd]) => dd);
|
|
41
|
+
}
|
|
42
|
+
/** Shows one request. Returns the element the caller updates with the time left. */
|
|
43
|
+
export function renderSummary(doc, el, s, technical) {
|
|
44
|
+
el.title.textContent = s.title;
|
|
45
|
+
el.amount.textContent = s.amount ?? '';
|
|
46
|
+
el.amount.hidden = !s.amount;
|
|
47
|
+
const dds = rows(doc, el.rows, [...s.rows, { label: 'Expires', value: '' }]);
|
|
48
|
+
el['message-box'].hidden = !s.message;
|
|
49
|
+
el['message-label'].textContent = s.message?.label ?? '';
|
|
50
|
+
el.message.textContent = s.message?.text ?? '';
|
|
51
|
+
el['terms-box'].hidden = s.terms.length === 0;
|
|
52
|
+
rows(doc, el.terms, s.terms);
|
|
53
|
+
el.warnings.replaceChildren(...s.warnings.map((w) => {
|
|
54
|
+
const li = doc.createElement('li');
|
|
55
|
+
li.textContent = w;
|
|
56
|
+
return li;
|
|
57
|
+
}));
|
|
58
|
+
el.warnings.hidden = s.warnings.length === 0;
|
|
59
|
+
el.note.textContent = s.note;
|
|
60
|
+
el.action.textContent = technical;
|
|
61
|
+
el.summary.hidden = false;
|
|
62
|
+
return dds[dds.length - 1];
|
|
63
|
+
}
|
|
64
|
+
export function showStatus(el, status) {
|
|
65
|
+
el.status.textContent = status.text;
|
|
66
|
+
el.status.className = `status ${status.tone}`;
|
|
67
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The NEAR wallet picker: the WAI-ARIA APG "select-only combobox" (a focusable combobox element
|
|
3
|
+
* and a listbox popup), so the list is styled by the page instead of drawn as an OS menu. The
|
|
4
|
+
* options are fixed here; the chosen value is still validated where it is used (login-wallet.ts).
|
|
5
|
+
*/
|
|
6
|
+
export interface WalletOption {
|
|
7
|
+
/** Element id of the option in the page shell. */
|
|
8
|
+
id: string;
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const WALLET_OPTIONS: readonly WalletOption[];
|
|
13
|
+
export interface PickerState {
|
|
14
|
+
open: boolean;
|
|
15
|
+
/** The highlighted option while open. */
|
|
16
|
+
active: number;
|
|
17
|
+
selected: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* One key on the combobox. Returns the next state, or undefined when the key is not handled.
|
|
21
|
+
* Closed: ArrowDown/ArrowUp/Enter/Space open on the selection, Home/End open on the first/last.
|
|
22
|
+
* Open: arrows, Home and End move; Enter, Space and Tab choose an enabled option; Escape closes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function pickerKey(s: PickerState, key: string, disabled: readonly boolean[]): PickerState | undefined;
|
|
25
|
+
/** Binds the picker to the page shell: `#near-wallet` (combobox) and `#near-wallet-list` (listbox). */
|
|
26
|
+
export declare function walletPicker(doc: Document, onChange: () => void): {
|
|
27
|
+
value: () => string;
|
|
28
|
+
/** Disable one option, e.g. HOT Wallet off mainnet. The selection is kept and refused on use. */
|
|
29
|
+
setDisabled(value: string, off: boolean): void;
|
|
30
|
+
/** While a wallet is open or the request is finished, the choice cannot change. */
|
|
31
|
+
setLocked(on: boolean): void;
|
|
32
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The NEAR wallet picker: the WAI-ARIA APG "select-only combobox" (a focusable combobox element
|
|
3
|
+
* and a listbox popup), so the list is styled by the page instead of drawn as an OS menu. The
|
|
4
|
+
* options are fixed here; the chosen value is still validated where it is used (login-wallet.ts).
|
|
5
|
+
*/
|
|
6
|
+
export const WALLET_OPTIONS = [
|
|
7
|
+
{ id: 'near-wallet-meteor', value: 'meteor-wallet', label: 'Meteor Wallet' },
|
|
8
|
+
{ id: 'near-wallet-hot', value: 'hot-wallet', label: 'HOT Wallet' },
|
|
9
|
+
];
|
|
10
|
+
/** The next enabled option from `from` in `step` direction, or `from` when there is none. */
|
|
11
|
+
function move(from, step, disabled) {
|
|
12
|
+
for (let i = from + step; i >= 0 && i < disabled.length; i += step)
|
|
13
|
+
if (!disabled[i])
|
|
14
|
+
return i;
|
|
15
|
+
return from;
|
|
16
|
+
}
|
|
17
|
+
const first = (disabled) => move(-1, 1, disabled);
|
|
18
|
+
const last = (disabled) => move(disabled.length, -1, disabled);
|
|
19
|
+
/**
|
|
20
|
+
* One key on the combobox. Returns the next state, or undefined when the key is not handled.
|
|
21
|
+
* Closed: ArrowDown/ArrowUp/Enter/Space open on the selection, Home/End open on the first/last.
|
|
22
|
+
* Open: arrows, Home and End move; Enter, Space and Tab choose an enabled option; Escape closes.
|
|
23
|
+
*/
|
|
24
|
+
export function pickerKey(s, key, disabled) {
|
|
25
|
+
if (!s.open) {
|
|
26
|
+
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(key))
|
|
27
|
+
return { ...s, open: true, active: s.selected };
|
|
28
|
+
if (key === 'Home')
|
|
29
|
+
return { ...s, open: true, active: first(disabled) };
|
|
30
|
+
if (key === 'End')
|
|
31
|
+
return { ...s, open: true, active: last(disabled) };
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (key === 'ArrowDown')
|
|
35
|
+
return { ...s, active: move(s.active, 1, disabled) };
|
|
36
|
+
if (key === 'ArrowUp')
|
|
37
|
+
return { ...s, active: move(s.active, -1, disabled) };
|
|
38
|
+
if (key === 'Home')
|
|
39
|
+
return { ...s, active: first(disabled) };
|
|
40
|
+
if (key === 'End')
|
|
41
|
+
return { ...s, active: last(disabled) };
|
|
42
|
+
if (key === 'Escape')
|
|
43
|
+
return { ...s, open: false };
|
|
44
|
+
if (key === 'Enter' || key === ' ' || key === 'Tab')
|
|
45
|
+
return {
|
|
46
|
+
...s,
|
|
47
|
+
open: false,
|
|
48
|
+
selected: disabled[s.active] ? s.selected : s.active,
|
|
49
|
+
};
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Binds the picker to the page shell: `#near-wallet` (combobox) and `#near-wallet-list` (listbox). */
|
|
53
|
+
export function walletPicker(doc, onChange) {
|
|
54
|
+
const combo = doc.getElementById('near-wallet');
|
|
55
|
+
const list = doc.getElementById('near-wallet-list');
|
|
56
|
+
const label = doc.getElementById('near-wallet-value');
|
|
57
|
+
const options = WALLET_OPTIONS.map((o) => doc.getElementById(o.id));
|
|
58
|
+
const disabled = WALLET_OPTIONS.map(() => false);
|
|
59
|
+
let state = { open: false, active: 0, selected: 0 };
|
|
60
|
+
let locked = false;
|
|
61
|
+
const render = () => {
|
|
62
|
+
combo.setAttribute('aria-expanded', String(state.open));
|
|
63
|
+
combo.setAttribute('aria-disabled', String(locked));
|
|
64
|
+
list.hidden = !state.open;
|
|
65
|
+
if (state.open)
|
|
66
|
+
combo.setAttribute('aria-activedescendant', WALLET_OPTIONS[state.active]?.id ?? '');
|
|
67
|
+
else
|
|
68
|
+
combo.removeAttribute('aria-activedescendant');
|
|
69
|
+
options.forEach((o, i) => {
|
|
70
|
+
o.setAttribute('aria-selected', String(i === state.selected));
|
|
71
|
+
o.setAttribute('aria-disabled', String(disabled[i]));
|
|
72
|
+
o.className = `option${i === state.active && state.open ? ' active' : ''}`;
|
|
73
|
+
});
|
|
74
|
+
label.textContent = WALLET_OPTIONS[state.selected]?.label ?? '';
|
|
75
|
+
};
|
|
76
|
+
const update = (next) => {
|
|
77
|
+
const changed = next.selected !== state.selected;
|
|
78
|
+
state = next;
|
|
79
|
+
render();
|
|
80
|
+
if (changed)
|
|
81
|
+
onChange();
|
|
82
|
+
};
|
|
83
|
+
combo.onkeydown = (e) => {
|
|
84
|
+
if (locked)
|
|
85
|
+
return;
|
|
86
|
+
const next = pickerKey(state, e.key, disabled);
|
|
87
|
+
if (!next)
|
|
88
|
+
return;
|
|
89
|
+
if (e.key !== 'Tab')
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
update(next);
|
|
92
|
+
};
|
|
93
|
+
combo.onclick = () => {
|
|
94
|
+
if (!locked)
|
|
95
|
+
update({ ...state, open: !state.open, active: state.selected });
|
|
96
|
+
};
|
|
97
|
+
options.forEach((o, i) => {
|
|
98
|
+
o.onclick = () => {
|
|
99
|
+
if (locked || disabled[i])
|
|
100
|
+
return;
|
|
101
|
+
update({ open: false, active: i, selected: i });
|
|
102
|
+
combo.focus();
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
// Close on any press outside the picker.
|
|
106
|
+
doc.addEventListener?.('pointerdown', (e) => {
|
|
107
|
+
const target = e.target;
|
|
108
|
+
if (state.open && !combo.contains(target) && !list.contains(target))
|
|
109
|
+
update({ ...state, open: false });
|
|
110
|
+
});
|
|
111
|
+
render();
|
|
112
|
+
return {
|
|
113
|
+
value: () => WALLET_OPTIONS[state.selected]?.value ?? '',
|
|
114
|
+
/** Disable one option, e.g. HOT Wallet off mainnet. The selection is kept and refused on use. */
|
|
115
|
+
setDisabled(value, off) {
|
|
116
|
+
const i = WALLET_OPTIONS.findIndex((o) => o.value === value);
|
|
117
|
+
if (i >= 0)
|
|
118
|
+
disabled[i] = off;
|
|
119
|
+
render();
|
|
120
|
+
},
|
|
121
|
+
/** While a wallet is open or the request is finished, the choice cannot change. */
|
|
122
|
+
setLocked(on) {
|
|
123
|
+
locked = on;
|
|
124
|
+
if (on)
|
|
125
|
+
state = { ...state, open: false };
|
|
126
|
+
render();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Plain-text formatting for the approval page: amounts, times and byte strings. */
|
|
2
|
+
/** Any value as plain text. Objects become JSON, so nothing is ever interpreted as markup. */
|
|
3
|
+
export declare const text: (v: unknown) => string;
|
|
4
|
+
export declare const record: (v: unknown) => Record<string, unknown>;
|
|
5
|
+
/** Base units as a decimal string, e.g. `formatUnits('200000', 6)` is `0.2`. Non-integers stay as given. */
|
|
6
|
+
export declare function formatUnits(value: unknown, decimals: number): string;
|
|
7
|
+
/** Minutes and seconds left, e.g. `1:05`; never negative. */
|
|
8
|
+
export declare function clock(ms: number): string;
|
|
9
|
+
/** A local date and time, e.g. `2026-11-14 14:02:11`. */
|
|
10
|
+
export declare function localTime(ms: number): string;
|
|
11
|
+
/** The request's own expiry, e.g. `1:45 left (until 14:02:11)` or `Expired`. */
|
|
12
|
+
export declare function expiryText(expiresAt: number, now: number): string;
|
|
13
|
+
/** A unix time in seconds (string or number) as local time, or the raw value when not one. */
|
|
14
|
+
export declare const secondsTime: (v: unknown) => string;
|
|
15
|
+
/** A 32-byte NEP-413 nonce as hex. Anything else is returned unchanged. */
|
|
16
|
+
export declare function nonceHex(nonce: unknown): unknown;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Plain-text formatting for the approval page: amounts, times and byte strings. */
|
|
2
|
+
/** Any value as plain text. Objects become JSON, so nothing is ever interpreted as markup. */
|
|
3
|
+
export const text = (v) => typeof v === 'string'
|
|
4
|
+
? v
|
|
5
|
+
: typeof v === 'number' || typeof v === 'bigint' || typeof v === 'boolean'
|
|
6
|
+
? String(v)
|
|
7
|
+
: v === undefined || v === null
|
|
8
|
+
? ''
|
|
9
|
+
: JSON.stringify(v);
|
|
10
|
+
export const record = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
11
|
+
/** Base units as a decimal string, e.g. `formatUnits('200000', 6)` is `0.2`. Non-integers stay as given. */
|
|
12
|
+
export function formatUnits(value, decimals) {
|
|
13
|
+
const raw = text(value);
|
|
14
|
+
if (!/^[0-9]+$/.test(raw))
|
|
15
|
+
return raw;
|
|
16
|
+
const digits = raw.replace(/^0+(?=.)/, '').padStart(decimals + 1, '0');
|
|
17
|
+
const whole = digits.slice(0, digits.length - decimals);
|
|
18
|
+
const fraction = digits.slice(digits.length - decimals).replace(/0+$/, '');
|
|
19
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
20
|
+
}
|
|
21
|
+
/** Minutes and seconds left, e.g. `1:05`; never negative. */
|
|
22
|
+
export function clock(ms) {
|
|
23
|
+
const s = Math.max(0, Math.ceil(ms / 1000));
|
|
24
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
|
25
|
+
}
|
|
26
|
+
/** A local date and time, e.g. `2026-11-14 14:02:11`. */
|
|
27
|
+
export function localTime(ms) {
|
|
28
|
+
const d = new Date(ms);
|
|
29
|
+
if (Number.isNaN(d.getTime()))
|
|
30
|
+
return '';
|
|
31
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
32
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
33
|
+
}
|
|
34
|
+
/** The request's own expiry, e.g. `1:45 left (until 14:02:11)` or `Expired`. */
|
|
35
|
+
export function expiryText(expiresAt, now) {
|
|
36
|
+
return now >= expiresAt
|
|
37
|
+
? 'Expired'
|
|
38
|
+
: `${clock(expiresAt - now)} left (until ${localTime(expiresAt).slice(11)})`;
|
|
39
|
+
}
|
|
40
|
+
/** A unix time in seconds (string or number) as local time, or the raw value when not one. */
|
|
41
|
+
export const secondsTime = (v) => {
|
|
42
|
+
const raw = text(v);
|
|
43
|
+
return /^[0-9]{1,12}$/.test(raw) ? `${localTime(Number(raw) * 1000)} local time` : raw;
|
|
44
|
+
};
|
|
45
|
+
/** A 32-byte NEP-413 nonce as hex. Anything else is returned unchanged. */
|
|
46
|
+
export function nonceHex(nonce) {
|
|
47
|
+
return Array.isArray(nonce) &&
|
|
48
|
+
nonce.length === 32 &&
|
|
49
|
+
nonce.every((n) => Number.isInteger(n) && n >= 0 && n < 256)
|
|
50
|
+
? `0x${nonce.map((n) => n.toString(16).padStart(2, '0')).join('')}`
|
|
51
|
+
: nonce;
|
|
52
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a wallet attempt failed, as a fixed reason the page shows and may report to the CLI. Only
|
|
3
|
+
* these reason names cross to the terminal, so no wallet- or page-supplied text is ever printed.
|
|
4
|
+
*/
|
|
5
|
+
export declare const WALLET_FAILURES: readonly ["window_closed", "popup_blocked", "rejected", "wallet_error", "refused", "send_uncertain"];
|
|
6
|
+
export type WalletFailure = (typeof WALLET_FAILURES)[number];
|
|
7
|
+
export declare const isWalletFailure: (v: unknown) => v is WalletFailure;
|
|
8
|
+
/** A refusal written by the page itself (wrong chain, wallet not offered); its message is shown as is. */
|
|
9
|
+
export declare class Refusal extends Error {
|
|
10
|
+
}
|
|
11
|
+
/** Any failure after the wallet's send call started, whatever the wallet says: it may have broadcast. */
|
|
12
|
+
export declare class SendUncertain extends Error {
|
|
13
|
+
}
|
|
14
|
+
/** The reason and the page text for a failed wallet attempt. */
|
|
15
|
+
export declare function walletFailure(error: unknown): {
|
|
16
|
+
reason: WalletFailure;
|
|
17
|
+
text: string;
|
|
18
|
+
};
|
|
19
|
+
export type Tone = 'idle' | 'wait' | 'ok' | 'error';
|
|
20
|
+
export interface Status {
|
|
21
|
+
tone: Tone;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const STATUS: {
|
|
25
|
+
readonly noCapability: {
|
|
26
|
+
readonly tone: "error";
|
|
27
|
+
readonly text: "Open the exact URL printed by your CLI.";
|
|
28
|
+
};
|
|
29
|
+
readonly waitingCli: {
|
|
30
|
+
readonly tone: "idle";
|
|
31
|
+
readonly text: "Waiting for the CLI to send a request.";
|
|
32
|
+
};
|
|
33
|
+
readonly ready: {
|
|
34
|
+
readonly tone: "idle";
|
|
35
|
+
readonly text: "Check the details, then continue in your wallet.";
|
|
36
|
+
};
|
|
37
|
+
readonly connect: {
|
|
38
|
+
readonly tone: "idle";
|
|
39
|
+
readonly text: "Transactions take two steps: connect your wallet first, then review the transaction.";
|
|
40
|
+
};
|
|
41
|
+
readonly wallet: {
|
|
42
|
+
readonly tone: "wait";
|
|
43
|
+
readonly text: "Waiting for your wallet. Finish or cancel there.";
|
|
44
|
+
};
|
|
45
|
+
readonly verifying: {
|
|
46
|
+
readonly tone: "wait";
|
|
47
|
+
readonly text: "Signed. The CLI is checking the result.";
|
|
48
|
+
};
|
|
49
|
+
readonly signed: {
|
|
50
|
+
readonly tone: "ok";
|
|
51
|
+
readonly text: "Signed. Return to the terminal. Keep this tab open if the command asks for another approval.";
|
|
52
|
+
};
|
|
53
|
+
readonly redirected: {
|
|
54
|
+
readonly tone: "ok";
|
|
55
|
+
readonly text: "Proof returned to the approval window. Return to your terminal.";
|
|
56
|
+
};
|
|
57
|
+
readonly rejected: {
|
|
58
|
+
readonly tone: "error";
|
|
59
|
+
readonly text: "The CLI did not accept the wallet result. Check the terminal for the reason.";
|
|
60
|
+
};
|
|
61
|
+
readonly expired: {
|
|
62
|
+
readonly tone: "error";
|
|
63
|
+
readonly text: "This request expired. Run the command again in the terminal.";
|
|
64
|
+
};
|
|
65
|
+
readonly ended: {
|
|
66
|
+
readonly tone: "error";
|
|
67
|
+
readonly text: "The approval session has ended. Return to the terminal.";
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export declare const connected: (account: string) => Status;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a wallet attempt failed, as a fixed reason the page shows and may report to the CLI. Only
|
|
3
|
+
* these reason names cross to the terminal, so no wallet- or page-supplied text is ever printed.
|
|
4
|
+
*/
|
|
5
|
+
export const WALLET_FAILURES = [
|
|
6
|
+
'window_closed',
|
|
7
|
+
'popup_blocked',
|
|
8
|
+
'rejected',
|
|
9
|
+
'wallet_error',
|
|
10
|
+
'refused',
|
|
11
|
+
/** The wallet's send call ran and then failed: the transaction may be on chain. Never retried. */
|
|
12
|
+
'send_uncertain',
|
|
13
|
+
];
|
|
14
|
+
export const isWalletFailure = (v) => WALLET_FAILURES.includes(v);
|
|
15
|
+
/** A refusal written by the page itself (wrong chain, wallet not offered); its message is shown as is. */
|
|
16
|
+
export class Refusal extends Error {
|
|
17
|
+
}
|
|
18
|
+
/** Any failure after the wallet's send call started, whatever the wallet says: it may have broadcast. */
|
|
19
|
+
export class SendUncertain extends Error {
|
|
20
|
+
}
|
|
21
|
+
const AGAIN = 'Try again or pick another wallet.';
|
|
22
|
+
/** The reason and the page text for a failed wallet attempt. */
|
|
23
|
+
export function walletFailure(error) {
|
|
24
|
+
if (error instanceof Refusal)
|
|
25
|
+
return { reason: 'refused', text: error.message };
|
|
26
|
+
if (error instanceof SendUncertain)
|
|
27
|
+
return {
|
|
28
|
+
reason: 'send_uncertain',
|
|
29
|
+
text: 'The wallet may have sent the transaction. Do not approve it again. Return to the terminal.',
|
|
30
|
+
};
|
|
31
|
+
const e = (error && typeof error === 'object' ? error : {});
|
|
32
|
+
const message = typeof error === 'string' ? error : String(e.message ?? '');
|
|
33
|
+
const tags = Array.isArray(e.endTags) ? e.endTags : [];
|
|
34
|
+
// Meteor's SDK: closed_window from its popup, WINDOW_CLOSED when the opener sees it closed, and
|
|
35
|
+
// POPUP_WINDOW_OPEN_FAILED when window.open returned nothing.
|
|
36
|
+
if (tags.includes('POPUP_WINDOW_OPEN_FAILED') || /open popup/i.test(message))
|
|
37
|
+
return {
|
|
38
|
+
reason: 'popup_blocked',
|
|
39
|
+
text: 'Your browser blocked the wallet window. Allow pop-ups for this page, then try again.',
|
|
40
|
+
};
|
|
41
|
+
if (tags.includes('WINDOW_CLOSED') || /closed the window|window closed/i.test(message))
|
|
42
|
+
return { reason: 'window_closed', text: `The wallet window closed before signing. ${AGAIN}` };
|
|
43
|
+
// EIP-1193 4001 is a user rejection.
|
|
44
|
+
if (e.code === 4001 || /reject|denied|declined|cancel/i.test(message))
|
|
45
|
+
return { reason: 'rejected', text: `The wallet declined the request. ${AGAIN}` };
|
|
46
|
+
return {
|
|
47
|
+
reason: 'wallet_error',
|
|
48
|
+
text: `The wallet could not complete the request${message ? ` (${message.slice(0, 200)})` : ''}. ${AGAIN}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export const STATUS = {
|
|
52
|
+
noCapability: { tone: 'error', text: 'Open the exact URL printed by your CLI.' },
|
|
53
|
+
waitingCli: { tone: 'idle', text: 'Waiting for the CLI to send a request.' },
|
|
54
|
+
ready: { tone: 'idle', text: 'Check the details, then continue in your wallet.' },
|
|
55
|
+
connect: {
|
|
56
|
+
tone: 'idle',
|
|
57
|
+
text: 'Transactions take two steps: connect your wallet first, then review the transaction.',
|
|
58
|
+
},
|
|
59
|
+
wallet: { tone: 'wait', text: 'Waiting for your wallet. Finish or cancel there.' },
|
|
60
|
+
verifying: { tone: 'wait', text: 'Signed. The CLI is checking the result.' },
|
|
61
|
+
signed: {
|
|
62
|
+
tone: 'ok',
|
|
63
|
+
text: 'Signed. Return to the terminal. Keep this tab open if the command asks for another approval.',
|
|
64
|
+
},
|
|
65
|
+
redirected: {
|
|
66
|
+
tone: 'ok',
|
|
67
|
+
text: 'Proof returned to the approval window. Return to your terminal.',
|
|
68
|
+
},
|
|
69
|
+
rejected: {
|
|
70
|
+
tone: 'error',
|
|
71
|
+
text: 'The CLI did not accept the wallet result. Check the terminal for the reason.',
|
|
72
|
+
},
|
|
73
|
+
expired: {
|
|
74
|
+
tone: 'error',
|
|
75
|
+
text: 'This request expired. Run the command again in the terminal.',
|
|
76
|
+
},
|
|
77
|
+
ended: {
|
|
78
|
+
tone: 'error',
|
|
79
|
+
text: 'The approval session has ended. Return to the terminal.',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
export const connected = (account) => ({
|
|
83
|
+
tone: 'idle',
|
|
84
|
+
text: `Connected as ${account}. Now review the transaction in your wallet.`,
|
|
85
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's payment or listing terms attached to a signing request (`SigningRequest.terms`). They
|
|
3
|
+
* are review context, not what the wallet signs: the page shows them apart from the signed request
|
|
4
|
+
* and warns when their price differs from the amount the request signs.
|
|
5
|
+
*/
|
|
6
|
+
import { formatUnits, record, text } from './login-format.js';
|
|
7
|
+
/** `{kind, id}` as `NEAR alice.near` or `EVM 0x...`; anything else as plain text. */
|
|
8
|
+
const identity = (v) => {
|
|
9
|
+
const i = record(v);
|
|
10
|
+
return typeof i.kind === 'string' && typeof i.id === 'string'
|
|
11
|
+
? `${i.kind === 'evm' ? 'EVM' : i.kind === 'near' ? 'NEAR' : i.kind === 'solana' ? 'Solana' : i.kind} ${i.id}`
|
|
12
|
+
: text(v);
|
|
13
|
+
};
|
|
14
|
+
const near = (v) => (/^[0-9]+$/.test(text(v)) ? `${formatUnits(v, 24)} NEAR` : text(v));
|
|
15
|
+
/** Split recipients with their share; the remainder recipient also gets the rounding rest. */
|
|
16
|
+
function split(recipients, remainder) {
|
|
17
|
+
if (!Array.isArray(recipients))
|
|
18
|
+
return [];
|
|
19
|
+
return recipients.map((r, i) => {
|
|
20
|
+
const bps = Number(record(r).bps);
|
|
21
|
+
const share = Number.isInteger(bps) ? `${(bps / 100).toFixed(2)}%` : text(record(r).bps);
|
|
22
|
+
return {
|
|
23
|
+
label: recipients.length === 1 ? 'Paid to' : `Recipient ${i + 1}`,
|
|
24
|
+
value: `${identity(record(r).identity)} ${share}${i === remainder ? ', plus the rounding remainder' : ''}`,
|
|
25
|
+
mono: true,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function describeTerms(terms, signedPrice) {
|
|
30
|
+
const t = record(terms);
|
|
31
|
+
if (!Object.keys(t).length)
|
|
32
|
+
return { rows: [], warnings: [] };
|
|
33
|
+
const s = record(t.split);
|
|
34
|
+
const source = record(t.source);
|
|
35
|
+
const optional = (label, v, mono = true, format = text) => v === undefined || v === null || v === '' ? [] : [{ label, value: format(v), mono }];
|
|
36
|
+
const rows = [
|
|
37
|
+
...optional('File', t.file),
|
|
38
|
+
...optional('Resource', t.resource),
|
|
39
|
+
...optional('Seller', t.seller, true, identity),
|
|
40
|
+
...optional('Host', t.host),
|
|
41
|
+
...optional('Contract', t.contract),
|
|
42
|
+
...optional('Route', t.route, false),
|
|
43
|
+
...optional('Price', t.price, false, (v) => `${formatUnits(v, 6)} USDC`),
|
|
44
|
+
...optional('Token', t.token),
|
|
45
|
+
...split(t.recipients ?? s.recipients, t.remainder ?? s.remainder),
|
|
46
|
+
...optional('Base payout address', t.baseRecipient),
|
|
47
|
+
...optional('Protocol fee', t.fee, false),
|
|
48
|
+
...optional('Listing storage', t.listingStorage, false, near),
|
|
49
|
+
...optional('Purchase storage', t.purchaseStorage, false, near),
|
|
50
|
+
...(Object.keys(source).length
|
|
51
|
+
? [
|
|
52
|
+
{
|
|
53
|
+
label: 'Pay from',
|
|
54
|
+
value: `${text(source.amountFormatted ?? source.amount)} ${text(source.symbol ?? source.asset)} on ${text(source.chain)}`,
|
|
55
|
+
},
|
|
56
|
+
...optional('Deposit before', source.depositBefore, false),
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
59
|
+
];
|
|
60
|
+
const price = text(t.price);
|
|
61
|
+
return {
|
|
62
|
+
rows,
|
|
63
|
+
warnings: signedPrice !== undefined && price && price !== signedPrice
|
|
64
|
+
? [
|
|
65
|
+
`The CLI's terms say ${formatUnits(price, 6)} USDC, but the request signs ${formatUnits(signedPrice, 6)}. Do not approve.`,
|
|
66
|
+
]
|
|
67
|
+
: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the approval page shows for one signing request. Every value comes from the request object
|
|
3
|
+
* the page signs (`operation`, `chain`, `account`, `expiresAt`, `payload`); `terms` is the CLI's
|
|
4
|
+
* review context and is shown apart from it, labelled as such. Pure functions: the DOM code sets
|
|
5
|
+
* each returned string as text, never as markup.
|
|
6
|
+
*/
|
|
7
|
+
import type { SigningRequest } from '../node/signing.js';
|
|
8
|
+
export interface Row {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
/** Addresses, hashes and other values a person compares character by character. */
|
|
12
|
+
mono?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface Summary {
|
|
15
|
+
title: string;
|
|
16
|
+
/** The headline amount of a payment, e.g. `0.2 USDC`. */
|
|
17
|
+
amount?: string;
|
|
18
|
+
rows: Row[];
|
|
19
|
+
/** The exact text a wallet signs, or the arguments of a transaction. */
|
|
20
|
+
message?: {
|
|
21
|
+
label: string;
|
|
22
|
+
text: string;
|
|
23
|
+
};
|
|
24
|
+
/** What signing does and does not do. */
|
|
25
|
+
note: string;
|
|
26
|
+
/** The CLI's payment or listing terms, when the request carries them. */
|
|
27
|
+
terms: Row[];
|
|
28
|
+
warnings: string[];
|
|
29
|
+
}
|
|
30
|
+
/** The whole request as JSON, with a NEP-413 nonce as hex instead of 32 numbers. */
|
|
31
|
+
export declare function technicalDetails(q: SigningRequest): string;
|
|
32
|
+
export declare const networkName: (chain: string) => string;
|
|
33
|
+
/** The summary of one signing request. Unknown operations are described as such, never guessed. */
|
|
34
|
+
export declare function describeRequest(q: SigningRequest): Summary;
|