@defdone/annon 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +18 -0
- package/CLI.md +58 -4
- package/README.md +6 -1
- package/dist/browser/login-bundle.js +51 -51
- package/dist/browser/login-dom.d.ts +1 -1
- package/dist/browser/login-dom.js +5 -1
- package/dist/browser/login-status.d.ts +1 -1
- package/dist/browser/login-status.js +1 -1
- package/dist/browser/login-steps.d.ts +6 -0
- package/dist/browser/login-steps.js +35 -0
- package/dist/browser/login.js +41 -4
- package/dist/node/adapter-checks.d.ts +3 -1
- package/dist/node/adapter-checks.js +3 -1
- package/dist/node/adapter.js +5 -1
- package/dist/node/agent.js +2 -1
- package/dist/node/browser-login.js +28 -3
- package/dist/node/buy-commands.js +9 -8
- package/dist/node/buy-flow.d.ts +18 -0
- package/dist/node/buy-flow.js +89 -0
- package/dist/node/cli-flags.d.ts +10 -0
- package/dist/node/cli-flags.js +68 -0
- package/dist/node/cli.d.ts +9 -1
- package/dist/node/cli.js +68 -92
- package/dist/node/config.d.ts +2 -0
- package/dist/node/config.js +3 -1
- package/dist/node/core-adapter.js +4 -0
- package/dist/node/login-page.d.ts +2 -2
- package/dist/node/login-page.js +7 -1
- package/dist/node/main.js +20 -2
- package/dist/node/output/discovery.d.ts +23 -0
- package/dist/node/output/discovery.js +52 -0
- package/dist/node/output/human.d.ts +10 -0
- package/dist/node/output/human.js +46 -0
- package/dist/node/output/purchase.d.ts +10 -0
- package/dist/node/output/purchase.js +78 -0
- package/dist/node/output/text.d.ts +10 -0
- package/dist/node/output/text.js +43 -0
- package/dist/node/purchase-commands.js +4 -0
- package/dist/node/purchase-ref.d.ts +15 -0
- package/dist/node/purchase-ref.js +93 -0
- package/dist/node/shell/complete.d.ts +2 -0
- package/dist/node/shell/complete.js +17 -0
- package/dist/node/shell/session.d.ts +20 -0
- package/dist/node/shell/session.js +219 -0
- package/dist/node/shell/welcome.d.ts +9 -0
- package/dist/node/shell/welcome.js +39 -0
- package/dist/node/shell/words.d.ts +1 -0
- package/dist/node/shell/words.js +36 -0
- package/dist/node/shops.d.ts +5 -0
- package/dist/node/shops.js +22 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Status } from './login-status.js';
|
|
6
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"];
|
|
7
|
+
declare const IDS: readonly ["current-technical", "step-state", "title", "pair", "summary", "amount", "rows", "message-box", "message-label", "message", "terms-box", "terms", "warnings", "note", "wallet-field", "approve", "status", "action"];
|
|
8
8
|
export type PageElements = Record<(typeof IDS)[number], HTMLElement>;
|
|
9
9
|
export declare function pageElements(doc: Document): PageElements;
|
|
10
10
|
/** Shows one request. Returns the element the caller updates with the time left. */
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* this module never parses markup.
|
|
4
4
|
*/
|
|
5
5
|
const IDS = [
|
|
6
|
+
'current-technical',
|
|
7
|
+
'step-state',
|
|
6
8
|
'title',
|
|
7
9
|
'pair',
|
|
8
10
|
'summary',
|
|
@@ -59,7 +61,9 @@ export function renderSummary(doc, el, s, technical) {
|
|
|
59
61
|
el.note.textContent = s.note;
|
|
60
62
|
el.action.textContent = technical;
|
|
61
63
|
el.summary.hidden = false;
|
|
62
|
-
|
|
64
|
+
const expiry = dds[dds.length - 1];
|
|
65
|
+
expiry.setAttribute('data-expiry', '');
|
|
66
|
+
return expiry;
|
|
63
67
|
}
|
|
64
68
|
export function showStatus(el, status) {
|
|
65
69
|
el.status.textContent = status.text;
|
|
@@ -48,7 +48,7 @@ export declare const STATUS: {
|
|
|
48
48
|
};
|
|
49
49
|
readonly signed: {
|
|
50
50
|
readonly tone: "ok";
|
|
51
|
-
readonly text: "Signed. Return to the terminal. Keep this tab open
|
|
51
|
+
readonly text: "Signed. Return to the terminal. Keep this tab open until the command finishes.";
|
|
52
52
|
};
|
|
53
53
|
readonly redirected: {
|
|
54
54
|
readonly tone: "ok";
|
|
@@ -60,7 +60,7 @@ export const STATUS = {
|
|
|
60
60
|
verifying: { tone: 'wait', text: 'Signed. The CLI is checking the result.' },
|
|
61
61
|
signed: {
|
|
62
62
|
tone: 'ok',
|
|
63
|
-
text: 'Signed. Return to the terminal. Keep this tab open
|
|
63
|
+
text: 'Signed. Return to the terminal. Keep this tab open until the command finishes.',
|
|
64
64
|
},
|
|
65
65
|
redirected: {
|
|
66
66
|
tone: 'ok',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Completed steps contain snapshots of text and review content, never live controls. */
|
|
2
|
+
export function approvalSteps(doc, el) {
|
|
3
|
+
const list = doc.getElementById('steps');
|
|
4
|
+
return {
|
|
5
|
+
start() {
|
|
6
|
+
el['current-technical'].hidden = false;
|
|
7
|
+
el['step-state'].textContent = 'In progress';
|
|
8
|
+
},
|
|
9
|
+
finish() {
|
|
10
|
+
const step = doc.createElement('details');
|
|
11
|
+
step.className = 'step done';
|
|
12
|
+
const heading = doc.createElement('summary');
|
|
13
|
+
const mark = doc.createElement('span');
|
|
14
|
+
mark.className = 'done-mark';
|
|
15
|
+
mark.textContent = '✓ Done';
|
|
16
|
+
const title = doc.createElement('span');
|
|
17
|
+
title.textContent = el.title.textContent;
|
|
18
|
+
heading.append(mark, title);
|
|
19
|
+
const content = el.summary.cloneNode(true);
|
|
20
|
+
// Snapshots have no IDs, so only the current request owns page references.
|
|
21
|
+
content.removeAttribute('id');
|
|
22
|
+
for (const node of Array.from(content.querySelectorAll('[id]')))
|
|
23
|
+
node.removeAttribute('id');
|
|
24
|
+
for (const node of Array.from(content.querySelectorAll('[data-expiry]')))
|
|
25
|
+
node.textContent = 'Signed';
|
|
26
|
+
const technical = doc.createElement('pre');
|
|
27
|
+
technical.textContent = el.action.textContent;
|
|
28
|
+
step.append(heading, content, technical);
|
|
29
|
+
list.append(step);
|
|
30
|
+
el.summary.hidden = true;
|
|
31
|
+
el['current-technical'].hidden = true;
|
|
32
|
+
el['step-state'].textContent = 'Done';
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/browser/login.js
CHANGED
|
@@ -3,6 +3,7 @@ import { pageElements, renderSummary, showStatus } from './login-dom.js';
|
|
|
3
3
|
import { walletPicker } from './login-dropdown.js';
|
|
4
4
|
import { expiryText } from './login-format.js';
|
|
5
5
|
import { connected, SendUncertain, STATUS, walletFailure } from './login-status.js';
|
|
6
|
+
import { approvalSteps } from './login-steps.js';
|
|
6
7
|
import { describeRequest, technicalDetails } from './login-view.js';
|
|
7
8
|
import { pageWallets } from './login-wallet.js';
|
|
8
9
|
Object.assign(globalThis, { Buffer });
|
|
@@ -13,7 +14,11 @@ const fragment = new URLSearchParams(location.hash.slice(1));
|
|
|
13
14
|
const redirected = location.pathname === '/callback';
|
|
14
15
|
let capability = redirected ? '' : location.hash.slice(1);
|
|
15
16
|
history.replaceState(null, '', location.pathname);
|
|
16
|
-
|
|
17
|
+
let paused = false;
|
|
18
|
+
let finished = false;
|
|
19
|
+
let pollTimer;
|
|
20
|
+
const steps = approvalSteps(document, el);
|
|
21
|
+
const api = async (path, body, accepted) => {
|
|
17
22
|
const r = await fetch(path, {
|
|
18
23
|
method: 'POST',
|
|
19
24
|
headers: { authorization: `Bearer ${capability}`, 'content-type': 'application/json' },
|
|
@@ -24,11 +29,13 @@ const api = async (path, body) => {
|
|
|
24
29
|
});
|
|
25
30
|
if (!r.ok)
|
|
26
31
|
throw new Error('Session expired or wallet result rejected. Return to the terminal.');
|
|
32
|
+
accepted?.();
|
|
27
33
|
return r.json();
|
|
28
34
|
};
|
|
29
35
|
/** Requests whose result went to the CLI, or that ended here: never signed again. */
|
|
30
36
|
const done = new Set();
|
|
31
37
|
const callbacks = new Map();
|
|
38
|
+
const submitting = new Set();
|
|
32
39
|
const wallets = pageWallets({
|
|
33
40
|
expect: (state, id) => callbacks.set(state, id),
|
|
34
41
|
forget: (state) => callbacks.delete(state),
|
|
@@ -60,12 +67,34 @@ function controls() {
|
|
|
60
67
|
q && wallets.needsConnect(q, walletId()) ? 'Connect wallet' : 'Review in wallet';
|
|
61
68
|
}
|
|
62
69
|
async function submit(id, result) {
|
|
70
|
+
if (submitting.has(id))
|
|
71
|
+
return;
|
|
72
|
+
submitting.add(id);
|
|
73
|
+
paused = true;
|
|
74
|
+
clearTimeout(pollTimer);
|
|
63
75
|
done.add(id);
|
|
64
76
|
status(STATUS.verifying);
|
|
65
77
|
controls();
|
|
66
78
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
let accepted = false;
|
|
80
|
+
try {
|
|
81
|
+
const next = await api('/api/result', { id, result, follow: true }, () => {
|
|
82
|
+
accepted = true;
|
|
83
|
+
steps.finish();
|
|
84
|
+
finished = true;
|
|
85
|
+
status(STATUS.signed);
|
|
86
|
+
});
|
|
87
|
+
if (next.action && next.action.id !== id) {
|
|
88
|
+
show(next.action);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (!accepted)
|
|
93
|
+
throw error;
|
|
94
|
+
status(STATUS.signed);
|
|
95
|
+
}
|
|
96
|
+
paused = false;
|
|
97
|
+
pollTimer = setTimeout(() => void poll(), 1000);
|
|
69
98
|
}
|
|
70
99
|
catch {
|
|
71
100
|
status(STATUS.rejected);
|
|
@@ -99,6 +128,8 @@ else {
|
|
|
99
128
|
function show(q, sendStarted = false) {
|
|
100
129
|
if (q && q.id !== current?.id) {
|
|
101
130
|
current = q;
|
|
131
|
+
finished = false;
|
|
132
|
+
steps.start();
|
|
102
133
|
expiry = renderSummary(document, el, describeRequest(q), technicalDetails(q));
|
|
103
134
|
el['wallet-field'].hidden = !q.operation.startsWith('near.');
|
|
104
135
|
picker.setDisabled('hot-wallet', q.chain !== 'near:mainnet');
|
|
@@ -125,19 +156,25 @@ function show(q, sendStarted = false) {
|
|
|
125
156
|
controls();
|
|
126
157
|
}
|
|
127
158
|
async function poll() {
|
|
159
|
+
if (paused)
|
|
160
|
+
return;
|
|
128
161
|
if (!capability) {
|
|
129
162
|
status(STATUS.noCapability);
|
|
130
163
|
return;
|
|
131
164
|
}
|
|
132
165
|
try {
|
|
133
166
|
const r = await api('/api/action');
|
|
167
|
+
if (paused)
|
|
168
|
+
return;
|
|
134
169
|
el.pair.textContent = String(r.pairingCode);
|
|
135
170
|
show(r.action ?? undefined, r.sendStarted === true);
|
|
136
|
-
setTimeout(() => {
|
|
171
|
+
pollTimer = setTimeout(() => {
|
|
137
172
|
void poll();
|
|
138
173
|
}, 1000);
|
|
139
174
|
}
|
|
140
175
|
catch (e) {
|
|
176
|
+
if (paused || finished)
|
|
177
|
+
return;
|
|
141
178
|
status({ tone: 'error', text: e.message });
|
|
142
179
|
capability = '';
|
|
143
180
|
approve.disabled = true;
|
|
@@ -3,4 +3,6 @@ import type { Config } from './config.js';
|
|
|
3
3
|
declare const fail: (code: AnnonErrorCode, message: string, exit?: PublicError["exitCode"]) => never;
|
|
4
4
|
declare const isId: (id: string) => boolean;
|
|
5
5
|
declare const backupPath: (c: Config, id: string) => string;
|
|
6
|
-
|
|
6
|
+
/** A catalog row number in place of a URL: `buy 2 --shop demo`. */
|
|
7
|
+
declare const isRow: (target: string | undefined) => boolean;
|
|
8
|
+
export { backupPath, fail, isId, isRow };
|
|
@@ -10,4 +10,6 @@ const backupPath = (c, id) => {
|
|
|
10
10
|
throw usage();
|
|
11
11
|
return join(c.stateDir, 'backups', `${id.replaceAll(':', '-')}.json`);
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
/** A catalog row number in place of a URL: `buy 2 --shop demo`. */
|
|
14
|
+
const isRow = (target) => /^[1-9][0-9]{0,4}$/.test(target ?? '');
|
|
15
|
+
export { backupPath, fail, isId, isRow };
|
package/dist/node/adapter.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createCoreAdapter } from './core-adapter.js';
|
|
3
3
|
// core-adapter.js loads first: it imports the SDK modules in the order this file did, so they initialize as before.
|
|
4
4
|
import { identityJson, parseIdentity } from '../settle-near/identity.js';
|
|
5
|
+
import { isRow } from './adapter-checks.js';
|
|
5
6
|
import { agentPolicy, configPath, initAgent, savePublicConfig } from './agent.js';
|
|
6
7
|
import { initBackupSecret, initPolicy } from './backup-secret-init.js';
|
|
7
8
|
import { startBrowserSession } from './browser-login.js';
|
|
@@ -9,7 +10,7 @@ import { loadConfig } from './config.js';
|
|
|
9
10
|
import { AdapterError } from './errors.js';
|
|
10
11
|
import { osKeychain } from './keychain.js';
|
|
11
12
|
import { confinePaths } from './paths.js';
|
|
12
|
-
import { discoverShops, isDiscovery } from './shops.js';
|
|
13
|
+
import { catalogRow, discoverShops, isDiscovery } from './shops.js';
|
|
13
14
|
import { assertSignerInput, configuredWallet } from './signer-wallet.js';
|
|
14
15
|
import { signingRequest, verifySigningResult } from './signing.js';
|
|
15
16
|
import { identity } from './wallets.js';
|
|
@@ -29,6 +30,9 @@ export function createAdapter(env = process.env, d = {}) {
|
|
|
29
30
|
const c = await loadConfig(o, env, o.command === 'login', d.agent);
|
|
30
31
|
if (agent || c.agent)
|
|
31
32
|
o = await confinePaths(o, c.workDir);
|
|
33
|
+
// Agents buy allowed resource URLs only; a row number stays a refused target for them.
|
|
34
|
+
else if (o.command === 'buy' && isRow(o.target))
|
|
35
|
+
o = await catalogRow(o, env, d.notify, d.fetch);
|
|
32
36
|
if (c.signer) {
|
|
33
37
|
assertSignerInput(o);
|
|
34
38
|
if (o.recovery && identityJson(identity(o.recovery)) !== identityJson(c.signer.identity))
|
package/dist/node/agent.js
CHANGED
|
@@ -47,6 +47,7 @@ export async function agentPolicy(o, env, forced = false) {
|
|
|
47
47
|
o.command === 'login' ||
|
|
48
48
|
(o.config !== undefined && resolve(o.config) !== resolve(pinned)) ||
|
|
49
49
|
o.stateDir !== undefined ||
|
|
50
|
+
o.open !== undefined ||
|
|
50
51
|
[
|
|
51
52
|
o.contractId,
|
|
52
53
|
o.nearRpc,
|
|
@@ -60,7 +61,7 @@ export async function agentPolicy(o, env, forced = false) {
|
|
|
60
61
|
o.listingStorage,
|
|
61
62
|
o.fundAmount,
|
|
62
63
|
].some((v) => v !== undefined)))
|
|
63
|
-
throw new AdapterError('agent_policy', 'Agent mode pins deployment, trust, price and state policy; per-call overrides, init and login are refused.', 2);
|
|
64
|
+
throw new AdapterError('agent_policy', 'Agent mode pins deployment, trust, price and state policy; per-call overrides, buy --open, init and login are refused.', 2);
|
|
64
65
|
return agent;
|
|
65
66
|
}
|
|
66
67
|
export async function savePublicConfig(path, patch) {
|
|
@@ -54,6 +54,12 @@ export async function startBrowserSession(o) {
|
|
|
54
54
|
const sent = (p) => p.sending === true ||
|
|
55
55
|
p.action.operation === 'near.signTransaction';
|
|
56
56
|
let pending;
|
|
57
|
+
const followers = new Set();
|
|
58
|
+
const advance = (action = null) => {
|
|
59
|
+
for (const response of followers)
|
|
60
|
+
response.end(JSON.stringify({ action }));
|
|
61
|
+
followers.clear();
|
|
62
|
+
};
|
|
57
63
|
const server = createServer((req, res) => {
|
|
58
64
|
void (async () => {
|
|
59
65
|
const headers = {
|
|
@@ -146,9 +152,16 @@ export async function startBrowserSession(o) {
|
|
|
146
152
|
armIdle();
|
|
147
153
|
}
|
|
148
154
|
notify(APPROVED_LINE);
|
|
149
|
-
p.resolve(b.result);
|
|
150
155
|
complete(200);
|
|
151
|
-
|
|
156
|
+
if (b.follow === true) {
|
|
157
|
+
res.writeHead(200, { ...headers, 'content-type': 'application/json' });
|
|
158
|
+
res.flushHeaders();
|
|
159
|
+
followers.add(res);
|
|
160
|
+
res.on('close', () => followers.delete(res));
|
|
161
|
+
}
|
|
162
|
+
else
|
|
163
|
+
send(200, '{}');
|
|
164
|
+
p.resolve(b.result);
|
|
152
165
|
}
|
|
153
166
|
catch (error) {
|
|
154
167
|
clearTimeout(p.timer);
|
|
@@ -198,8 +211,19 @@ export async function startBrowserSession(o) {
|
|
|
198
211
|
pending.reject(new Error('session closed'));
|
|
199
212
|
pending = undefined;
|
|
200
213
|
}
|
|
214
|
+
const flushed = [...followers].map((response) => new Promise((resolve) => response.end(JSON.stringify({ action: null }), resolve)));
|
|
215
|
+
followers.clear();
|
|
216
|
+
const stopped = new Promise((resolve) => server.close(() => resolve()));
|
|
217
|
+
let flushTimer;
|
|
218
|
+
await Promise.race([
|
|
219
|
+
Promise.all(flushed),
|
|
220
|
+
new Promise((resolve) => {
|
|
221
|
+
flushTimer = setTimeout(resolve, 1000);
|
|
222
|
+
}),
|
|
223
|
+
]);
|
|
224
|
+
clearTimeout(flushTimer);
|
|
201
225
|
server.closeAllConnections();
|
|
202
|
-
await
|
|
226
|
+
await stopped;
|
|
203
227
|
};
|
|
204
228
|
let timeout;
|
|
205
229
|
const armIdle = () => {
|
|
@@ -251,6 +275,7 @@ export async function startBrowserSession(o) {
|
|
|
251
275
|
}, action.expiresAt - Date.now()),
|
|
252
276
|
};
|
|
253
277
|
pending = entry;
|
|
278
|
+
advance(entry.action);
|
|
254
279
|
notify(waitingLine(action.expiresAt - Date.now()));
|
|
255
280
|
}),
|
|
256
281
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Buyer commands before a purchase exists: `verify` a file and `buy` it. */
|
|
2
|
-
import { mkdir,
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { inspect, inspectHeader, MAX_HEADER, parseHeader } from '../envelope/pay2.js';
|
|
5
5
|
import { AnnonError } from '../errors.js';
|
|
@@ -17,6 +17,7 @@ import { offer, resourceHeader, response } from './offers.js';
|
|
|
17
17
|
import { originFor } from './origin.js';
|
|
18
18
|
import { humanPaymentTerms, quotedSource, savedPayment, validatePaymentToken, } from './payment-terms.js';
|
|
19
19
|
import { claimPurchase } from './purchase-guard.js';
|
|
20
|
+
import { rememberPurchase, savedRecordIds } from './purchase-ref.js';
|
|
20
21
|
import { resourceAllowed } from './resources.js';
|
|
21
22
|
/** `verify`: a local file's header and body, or a remote file's header, against its listing. */
|
|
22
23
|
async function verifyFile(ctx) {
|
|
@@ -155,13 +156,10 @@ async function buy(ctx, preparedBuy) {
|
|
|
155
156
|
const guardDir = join(c.stateDir, 'purchase-guards');
|
|
156
157
|
await mkdir(guardDir, { recursive: true, mode: 0o700 });
|
|
157
158
|
const known = [];
|
|
158
|
-
for (const
|
|
159
|
-
if (
|
|
159
|
+
for (const id of await savedRecordIds(storeDir)) {
|
|
160
|
+
if (!isId(id))
|
|
160
161
|
continue;
|
|
161
|
-
const
|
|
162
|
-
if (!isId(envelope.id))
|
|
163
|
-
continue;
|
|
164
|
-
const saved = await sdk.store.load(purchaseKey(envelope.id));
|
|
162
|
+
const saved = await sdk.store.load(purchaseKey(id));
|
|
165
163
|
if (saved?.purchase.file_id === found.fileId &&
|
|
166
164
|
saved.purchase.contract === c.contractId &&
|
|
167
165
|
saved.terms.network === (saved.purchase.route === 'base-usdc' ? 'eip155:8453' : c.network))
|
|
@@ -216,6 +214,7 @@ async function buy(ctx, preparedBuy) {
|
|
|
216
214
|
if (renewalError)
|
|
217
215
|
throw renewalError;
|
|
218
216
|
await guard.saved(record.id);
|
|
217
|
+
await rememberPurchase(c, record.id);
|
|
219
218
|
},
|
|
220
219
|
recovery: buyer,
|
|
221
220
|
...(baseSponsorship ? { baseSponsorship } : {}),
|
|
@@ -288,7 +287,9 @@ async function buy(ctx, preparedBuy) {
|
|
|
288
287
|
...(source ? { source } : {}),
|
|
289
288
|
next: bought.uncertain
|
|
290
289
|
? `The wallet may have sent this payment. Do not pay again. Run annon status ${bought.record.id} until it is accepted.`
|
|
291
|
-
:
|
|
290
|
+
: bought.instructions
|
|
291
|
+
? 'Fund the deposit as instructed, then check status and open this purchase.'
|
|
292
|
+
: `Payment sent. Run annon status ${bought.record.id}, then annon open ${bought.record.id} --output <file>.`,
|
|
292
293
|
};
|
|
293
294
|
}
|
|
294
295
|
catch (error) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Input } from './cli.js';
|
|
2
|
+
export interface BuyFollow {
|
|
3
|
+
notify?: (message: string) => void;
|
|
4
|
+
/** Pause between status checks; resolves early when `signal` aborts. */
|
|
5
|
+
wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
6
|
+
/** Aborting stops the waiting only. The saved purchase is left as it is. */
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
/** Asked once the purchase is accepted and no `--open` path was given. */
|
|
9
|
+
savePath?: () => Promise<string | undefined>;
|
|
10
|
+
onWaiting?: (waiting: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
type Execute = (input: Input) => Promise<unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Runs one command. A `buy` is followed when the caller is a person (`follow`) or asked for
|
|
15
|
+
* `--open`; a deposit route returns its instructions without waiting.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buyFlow(input: Input, execute: Execute, follow: boolean, o?: BuyFollow): Promise<unknown>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { commonInput } from './cli-flags.js';
|
|
2
|
+
import { publicError } from './errors.js';
|
|
3
|
+
import { newOutput } from './io.js';
|
|
4
|
+
const CHECKS = 60;
|
|
5
|
+
const INTERVAL_MS = 3000;
|
|
6
|
+
const pause = (ms, signal) => new Promise((resolve) => {
|
|
7
|
+
const done = () => {
|
|
8
|
+
clearTimeout(timer);
|
|
9
|
+
resolve();
|
|
10
|
+
};
|
|
11
|
+
const timer = setTimeout(done, ms);
|
|
12
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
13
|
+
});
|
|
14
|
+
/** Status checks until the purchase is accepted, the checks run out or the wait is cancelled. */
|
|
15
|
+
async function accepted(execute, input, id, o) {
|
|
16
|
+
o.onWaiting?.(true);
|
|
17
|
+
try {
|
|
18
|
+
for (let check = 0; check < CHECKS && !o.signal?.aborted; check++) {
|
|
19
|
+
if (check > 0)
|
|
20
|
+
await (o.wait ?? pause)(INTERVAL_MS, o.signal);
|
|
21
|
+
if (o.signal?.aborted)
|
|
22
|
+
break;
|
|
23
|
+
const status = (await execute({
|
|
24
|
+
...commonInput(input),
|
|
25
|
+
command: 'status',
|
|
26
|
+
target: id,
|
|
27
|
+
}));
|
|
28
|
+
if (status.state === 'accepted')
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
o.onWaiting?.(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Runs one command. A `buy` is followed when the caller is a person (`follow`) or asked for
|
|
39
|
+
* `--open`; a deposit route returns its instructions without waiting.
|
|
40
|
+
*/
|
|
41
|
+
export async function buyFlow(input, execute, follow, o = {}) {
|
|
42
|
+
if (input.command !== 'buy' || (!follow && input.open === undefined))
|
|
43
|
+
return execute(input);
|
|
44
|
+
// Refuse a taken output name before any payment step.
|
|
45
|
+
if (input.open !== undefined)
|
|
46
|
+
await newOutput(input.open);
|
|
47
|
+
const bought = (await execute(input));
|
|
48
|
+
const id = bought.purchaseId;
|
|
49
|
+
if (typeof id !== 'string' || !['submitted', 'uncertain'].includes(String(bought.state)))
|
|
50
|
+
return bought;
|
|
51
|
+
o.notify?.(bought.state === 'uncertain'
|
|
52
|
+
? 'The wallet may have sent this payment. Do not pay again. Checking NEAR...'
|
|
53
|
+
: 'Payment sent. Waiting for NEAR to accept...');
|
|
54
|
+
// From here the payment exists: a later failure is reported next to the purchase, never instead.
|
|
55
|
+
const pending = (statusError) => ({
|
|
56
|
+
...bought,
|
|
57
|
+
settlement: 'pending',
|
|
58
|
+
...(statusError ? { statusError } : {}),
|
|
59
|
+
next: `Not accepted yet. Do not pay again. Run annon status ${id} until it is accepted, then annon open ${id} --output <file>.`,
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
if (!(await accepted(execute, input, id, o)))
|
|
63
|
+
return pending();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return pending(publicError(error));
|
|
67
|
+
}
|
|
68
|
+
const settled = { ...bought, state: 'accepted', next: `annon open ${id} --output <file>` };
|
|
69
|
+
const output = input.open ?? (await o.savePath?.());
|
|
70
|
+
if (!output)
|
|
71
|
+
return settled;
|
|
72
|
+
try {
|
|
73
|
+
const opened = (await execute({
|
|
74
|
+
...commonInput(input),
|
|
75
|
+
command: 'open',
|
|
76
|
+
target: id,
|
|
77
|
+
output,
|
|
78
|
+
}));
|
|
79
|
+
return {
|
|
80
|
+
...bought,
|
|
81
|
+
state: 'opened',
|
|
82
|
+
opened: { output: opened.output, bytes: opened.bytes },
|
|
83
|
+
next: 'The file is saved.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return { ...settled, openError: publicError(error) };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The flag tables of the CLI: what every command accepts, for parsing, completion and follow-ups. */
|
|
2
|
+
import type { Command, Input } from './cli.js';
|
|
3
|
+
export declare const common: string[];
|
|
4
|
+
export declare const perCommand: Record<Command, string[]>;
|
|
5
|
+
export declare const camel: (flag: string) => string;
|
|
6
|
+
export declare const commandNames: Command[];
|
|
7
|
+
/** Every flag a command accepts, as typed on the command line. */
|
|
8
|
+
export declare const commandFlags: (command: Command) => string[];
|
|
9
|
+
/** The deployment, wallet and output options of one command, for a follow-up command it starts. */
|
|
10
|
+
export declare const commonInput: (input: Input) => Partial<Input>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const common = [
|
|
2
|
+
'no-open',
|
|
3
|
+
'json',
|
|
4
|
+
'config',
|
|
5
|
+
'contract-id',
|
|
6
|
+
'near-rpc',
|
|
7
|
+
'base-rpc',
|
|
8
|
+
'network',
|
|
9
|
+
'state-dir',
|
|
10
|
+
'relayer',
|
|
11
|
+
'trust-contract',
|
|
12
|
+
'wallet',
|
|
13
|
+
'evm-key-file',
|
|
14
|
+
'solana-key-file',
|
|
15
|
+
'near-key-file',
|
|
16
|
+
'near-account',
|
|
17
|
+
'near-public-key',
|
|
18
|
+
'backup-secret-file',
|
|
19
|
+
];
|
|
20
|
+
export const perCommand = {
|
|
21
|
+
shops: ['search', 'limit', 'offset'],
|
|
22
|
+
shop: ['id'],
|
|
23
|
+
catalog: ['shop', 'search', 'limit', 'offset'],
|
|
24
|
+
fund: [],
|
|
25
|
+
budget: [],
|
|
26
|
+
login: [],
|
|
27
|
+
init: [
|
|
28
|
+
'signer',
|
|
29
|
+
'credential-env',
|
|
30
|
+
'credential-keychain',
|
|
31
|
+
'agent',
|
|
32
|
+
'recovery',
|
|
33
|
+
'key-env',
|
|
34
|
+
'keychain-entry',
|
|
35
|
+
'new-key',
|
|
36
|
+
'daily-budget',
|
|
37
|
+
],
|
|
38
|
+
share: ['resume', 'price', 'shop', 'title', 'description', 'storage-budget', 'listing-storage'],
|
|
39
|
+
sell: ['resume', 'price', 'split', 'base', 'moderator', 'output', 'storage-budget'],
|
|
40
|
+
buy: [
|
|
41
|
+
'force',
|
|
42
|
+
'origin',
|
|
43
|
+
'route',
|
|
44
|
+
'from-chain',
|
|
45
|
+
'from-asset',
|
|
46
|
+
'refund-to',
|
|
47
|
+
'recovery',
|
|
48
|
+
'max-price',
|
|
49
|
+
'moderator',
|
|
50
|
+
'url',
|
|
51
|
+
'open',
|
|
52
|
+
'shop',
|
|
53
|
+
'search',
|
|
54
|
+
],
|
|
55
|
+
open: ['purchase', 'file', 'url', 'output'],
|
|
56
|
+
recover: ['recovery', 'purchase'],
|
|
57
|
+
stop: [],
|
|
58
|
+
verify: [],
|
|
59
|
+
status: [],
|
|
60
|
+
};
|
|
61
|
+
export const camel = (flag) => flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
62
|
+
export const commandNames = Object.keys(perCommand);
|
|
63
|
+
/** Every flag a command accepts, as typed on the command line. */
|
|
64
|
+
export const commandFlags = (command) => [...perCommand[command], ...common].map((flag) => `--${flag}`);
|
|
65
|
+
/** The deployment, wallet and output options of one command, for a follow-up command it starts. */
|
|
66
|
+
export const commonInput = (input) => Object.fromEntries(common
|
|
67
|
+
.map((flag) => [camel(flag), input[camel(flag)]])
|
|
68
|
+
.filter(([, v]) => v !== undefined));
|
package/dist/node/cli.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BuyRoute } from '../routes.js';
|
|
2
|
+
import { type BuyFollow } from './buy-flow.js';
|
|
2
3
|
export type Command = 'shops' | 'shop' | 'catalog' | 'fund' | 'budget' | 'share' | 'sell' | 'buy' | 'open' | 'recover' | 'stop' | 'verify' | 'status' | 'login' | 'init';
|
|
3
4
|
export interface Input {
|
|
4
5
|
command: Command;
|
|
@@ -55,6 +56,13 @@ export interface Input {
|
|
|
55
56
|
file?: string;
|
|
56
57
|
url?: string;
|
|
57
58
|
output?: string;
|
|
59
|
+
/** `buy --open <path>`: wait for acceptance, then write the opened file there. */
|
|
60
|
+
open?: string;
|
|
58
61
|
}
|
|
59
62
|
export declare function parseCli(argv: string[]): Input;
|
|
60
|
-
|
|
63
|
+
/** How one command line is presented and followed up; the defaults are the script contract. */
|
|
64
|
+
export interface RunOptions extends BuyFollow {
|
|
65
|
+
/** Readable text in place of the JSON envelope. `--json` always wins. */
|
|
66
|
+
human?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export declare function runCli(argv: string[], execute: (input: Input) => Promise<unknown>, write: (text: string) => void, options?: RunOptions): Promise<number>;
|