@provablehq/shield-swap-cli 0.7.0

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.
@@ -0,0 +1,229 @@
1
+ import {
2
+ help
3
+ } from "./chunk-IHYFMX5A.js";
4
+ import {
5
+ NeedsConfigDecisionError,
6
+ credentialsPath,
7
+ ensureKeyMaterial,
8
+ formatAmount,
9
+ loadSession,
10
+ loadState,
11
+ pollUntil,
12
+ resolveNetwork,
13
+ saveState,
14
+ stateDir
15
+ } from "./chunk-2OT6LZPW.js";
16
+
17
+ // src/commands/setup.ts
18
+ import { readFileSync } from "fs";
19
+ import { ApiError, DEFAULT_API_URL } from "@provablehq/shield-swap-sdk";
20
+ import { fileCredentialStore } from "@provablehq/veil-aleo-sdk/node";
21
+ var USAGE = `shield-swap setup \u2014 bootstrap an account and get it funded
22
+
23
+ --new generate a brand-new account
24
+ --private-key-file <path> import an existing key, read from this file
25
+ --consumer-id <id> Provable API consumer id (else self-registers)
26
+ --api-key <key> Provable API key
27
+ --invite-code <code> redeem an invite code when access is locked
28
+ --api-url <origin> pin a DEX API deployment
29
+ --network <testnet|mainnet> default testnet
30
+
31
+ Every step is check-then-act, so re-running resumes where a failed run stopped.
32
+
33
+ A private key is NEVER pasted into a conversation or command history: write it
34
+ to a file and pass --private-key-file, or export SHIELD_SWAP_PRIVATE_KEY in your
35
+ own shell. Other environment fallbacks: SHIELD_SWAP_PRIVATE_KEY_FILE,
36
+ ALEO_CONSUMER_ID, ALEO_DPS_API_KEY, SHIELD_SWAP_INVITE_CODE, SHIELD_SWAP_API_URL.
37
+
38
+ Exit codes: 0 ready \xB7 2 needs input from the user \xB7 3 airdrop still pending \xB7
39
+ 1 anything else.`;
40
+ function argValue(argv, flag) {
41
+ const i = argv.indexOf(flag);
42
+ return i >= 0 ? argv[i + 1] : void 0;
43
+ }
44
+ function resolveImportKey(argv) {
45
+ const keyFile = argValue(argv, "--private-key-file") ?? process.env.SHIELD_SWAP_PRIVATE_KEY_FILE;
46
+ if (keyFile) {
47
+ const key = readFileSync(keyFile, "utf8").trim();
48
+ if (!key) throw new Error(`private key file ${keyFile} is empty`);
49
+ return key;
50
+ }
51
+ return process.env.SHIELD_SWAP_PRIVATE_KEY;
52
+ }
53
+ async function setup(argv) {
54
+ const inviteCode = argValue(argv, "--invite-code") ?? process.env.SHIELD_SWAP_INVITE_CODE;
55
+ const consumerId = argValue(argv, "--consumer-id") ?? process.env.ALEO_CONSUMER_ID;
56
+ const apiKey = argValue(argv, "--api-key") ?? process.env.ALEO_DPS_API_KEY;
57
+ const network = resolveNetwork(argValue(argv, "--network"));
58
+ const apiUrl = argValue(argv, "--api-url")?.replace(/\/$/, "");
59
+ const credentialStore = fileCredentialStore(credentialsPath(network));
60
+ const allowGenerate = argv.includes("--new");
61
+ const importKey = resolveImportKey(argv);
62
+ let state = loadState(network);
63
+ console.log(`network: ${network} \xB7 state: ${stateDir(network)}`);
64
+ if (apiUrl && apiUrl !== (state.apiUrl ?? DEFAULT_API_URL)) {
65
+ state.apiUrl = apiUrl;
66
+ state.dexApiToken = void 0;
67
+ state.accessRedeemed = void 0;
68
+ state.airdropJobId = void 0;
69
+ saveState(state);
70
+ console.log(`\u2713 DEX API pinned to ${state.apiUrl} (deployment-scoped state reset)`);
71
+ }
72
+ try {
73
+ state = await ensureKeyMaterial(state, { importKey, allowGenerate });
74
+ } catch (err) {
75
+ if (err instanceof NeedsConfigDecisionError) {
76
+ console.error(
77
+ "\nNEEDS_CONFIG_DECISION: no shield-swap account is configured here. Ask the user whether they already have one before creating anything. NEVER ask them to paste a private key into the conversation:\n - existing account \u2192 the user saves their key to a file themselves, then re-run\n with --private-key-file <path> (or they export SHIELD_SWAP_PRIVATE_KEY in\n their own shell). Add --consumer-id/--api-key if they have Provable API\n credentials.\n - brand new \u2192 re-run with --new\n"
78
+ );
79
+ process.exit(2);
80
+ }
81
+ throw err;
82
+ }
83
+ console.log(`\u2713 account: ${state.address}`);
84
+ if (consumerId && apiKey && !await credentialStore.load()) {
85
+ await credentialStore.save({ consumerId, apiKey });
86
+ }
87
+ const legacy = state.provableApi;
88
+ if (legacy && !await credentialStore.load()) {
89
+ await credentialStore.save(legacy);
90
+ delete state.provableApi;
91
+ saveState(state);
92
+ console.log(`\u2713 moved Provable API credentials to ${credentialsPath(network)}`);
93
+ }
94
+ const { client, account } = await loadSession({ network });
95
+ console.log("\u2713 DEX API session established (challenge/verify)");
96
+ const provable = await client.authenticateProvableApi();
97
+ console.log(
98
+ `\u2713 Provable API consumer: ${provable.credentials.consumerId}` + (provable.registered ? ` (registered, saved to ${credentialsPath(network)})` : "")
99
+ );
100
+ const status = await client.api.getAccessStatus();
101
+ if (!status.has_access) {
102
+ if (!inviteCode) {
103
+ console.error(
104
+ "\nNEEDS_INVITE_CODE: this account has not redeemed an invite code, so the DEX API is locked. Ask the user for their invite code, then re-run:\n shield-swap setup --invite-code <code>\n"
105
+ );
106
+ process.exit(2);
107
+ }
108
+ let redeemed = false;
109
+ for (const attempt of [
110
+ () => client.api.redeemAccessCode(inviteCode),
111
+ () => client.api.redeemReferralCode(inviteCode)
112
+ ]) {
113
+ try {
114
+ await attempt();
115
+ redeemed = true;
116
+ break;
117
+ } catch (err) {
118
+ if (err instanceof ApiError && err.status === 400) continue;
119
+ throw err;
120
+ }
121
+ }
122
+ if (!redeemed) {
123
+ console.error(
124
+ `
125
+ INVALID_INVITE_CODE: the server rejected "${inviteCode}" as both an access code and a referral code. Ask the user for a valid, unused code and re-run.
126
+ `
127
+ );
128
+ process.exit(2);
129
+ }
130
+ state.accessRedeemed = true;
131
+ saveState(state);
132
+ console.log("\u2713 code redeemed \u2014 access unlocked");
133
+ } else {
134
+ state.accessRedeemed = true;
135
+ saveState(state);
136
+ console.log("\u2713 access already granted");
137
+ }
138
+ if (!state.dexApiToken) {
139
+ const created = await client.api.createApiToken({ name: `ss-agent-${account.address.slice(5, 17)}` });
140
+ state.dexApiToken = created.token;
141
+ saveState(state);
142
+ console.log(`\u2713 minted DEX API token (${created.token_prefix}\u2026, stored in state file)`);
143
+ } else {
144
+ console.log("\u2713 DEX API token already on file");
145
+ }
146
+ const funded = async () => {
147
+ const balances2 = await client.getBalances();
148
+ return Object.values(balances2).some((b) => b.total > 0n);
149
+ };
150
+ if (await funded()) {
151
+ console.log("\u2713 account already funded");
152
+ } else if (network === "mainnet") {
153
+ throw new Error(
154
+ `account ${account.address} holds no tokens on mainnet, and there is no faucet to draw from. Fund it from an exchange or another wallet, then re-run this script to verify.`
155
+ );
156
+ } else {
157
+ if (!state.airdropJobId) {
158
+ const job2 = await client.api.airdrop(account.address);
159
+ state.airdropJobId = job2.job_id;
160
+ saveState(state);
161
+ console.log(`\u2026 airdrop started (job ${job2.job_id})`);
162
+ } else {
163
+ console.log(`\u2026 resuming airdrop job ${state.airdropJobId}`);
164
+ }
165
+ let job = null;
166
+ const jobDone = await pollUntil(async () => {
167
+ job = await client.api.getAirdropStatus(state.airdropJobId).catch(() => null);
168
+ return job?.status === "complete";
169
+ }, 24, 5e3);
170
+ if (!jobDone) {
171
+ console.error(
172
+ `
173
+ AIRDROP_PENDING: faucet job ${state.airdropJobId} has not completed yet (last status: ${job ? job.status : "unknown"}). Re-run \`shield-swap setup\` in a few minutes \u2014 it resumes this job, it does not double-request.
174
+ `
175
+ );
176
+ process.exit(3);
177
+ }
178
+ const rejected = (job.results ?? []).filter((r) => r.status !== "accepted");
179
+ if (rejected.length > 0) {
180
+ state.airdropJobId = void 0;
181
+ saveState(state);
182
+ console.error(
183
+ `
184
+ AIRDROP_FAILED: faucet job finished with rejected transfers: ${rejected.map((r) => `${r.symbol}:${r.status}`).join(", ")}. Re-run \`shield-swap setup\` to request a fresh airdrop.
185
+ `
186
+ );
187
+ process.exit(3);
188
+ }
189
+ console.log("\u2026 faucet job complete \u2014 waiting for the records to become scannable");
190
+ const landed = await pollUntil(funded, 36, 1e4);
191
+ if (!landed) {
192
+ console.error(
193
+ "\nAIRDROP_PENDING: the faucet finished but the records are not scannable yet (the record service indexes asynchronously). Re-run `shield-swap setup` in a few minutes \u2014 it resumes this job, it does not double-request.\n"
194
+ );
195
+ process.exit(3);
196
+ }
197
+ console.log("\u2713 airdrop landed");
198
+ }
199
+ const balances = await client.getBalances();
200
+ console.log(`
201
+ Account ${account.address} is ready on ${network}:`);
202
+ for (const entry of Object.values(balances)) {
203
+ if (entry.total > 0n) {
204
+ const priv = formatAmount(entry.private, entry.decimals, entry.symbol);
205
+ const pub = formatAmount(entry.public, entry.decimals, entry.symbol);
206
+ console.log(` ${entry.symbol}: ${priv} private, ${pub} public`);
207
+ }
208
+ }
209
+ console.log(
210
+ `
211
+ ASK_NEXT_ACTION: setup is complete \u2014 ask the user what to do next with ONE user-selectable prompt (the harness's selection UI if it has one, a numbered list otherwise) offering ALL SEVEN options below, in this order. Free-form input stays available as the escape hatch ("Other"); map whatever the user types onto the runbooks before improvising against the SDK. Do not pick for them. Frame the setting first: Shield Swap is a private exchange on Aleo \u2014 what is traded, and by whom, stays hidden on the public chain. ` + (network === "mainnet" ? "This account is on MAINNET: every trade below moves real funds, and there is no faucet to recover from a mistake. Say so before the user picks." : "This account is on the test network, so trading uses test tokens.") + "\n\n1. Develop on Shield Swap (developing.md). For a user building their own\n dApp, trading bot, or server/agent integration rather than trading\n here. If chosen, FIRST ask what they are building, then follow\n developing.md \u2014 it picks the packages by where their keys live and\n maps to the docs, examples, and integration caveats.\n2. Follow their own playbook. Ask whether they have instructions of\n their own \u2014 a markdown strategy file, notes, or a memory store such\n as an Obsidian vault. Their document decides WHAT to do; the runbooks\n here describe HOW each step works.\n3. Swap tokens (swapping.md). Trade one token for another. It settles\n in two steps \u2014 placing the trade, then collecting what was bought \u2014\n and both happen in one go. The natural first move for a trader.\n4. Several swaps at once (swapping.md, concurrency recipe). Place a\n handful of trades in parallel and watch them all land \u2014 the busiest\n way to exercise the exchange. First show the user which trades are\n possible right now and ask how many (and which) they want; collect\n each one as it lands.\n5. Open a liquidity position (liquidity.md). Instead of trading, become\n the market: deposit a pair of tokens so other people can trade against\n them. The user picks the price range their deposit works in, and while\n the market price sits inside that range they earn a small cut of every\n trade that passes through.\n6. Add or remove liquidity (liquidity.md). Top up a position, or take\n some of it back out \u2014 whatever comes out becomes earnings to collect.\n7. Collect earnings (collecting.md). Sweep up everything the account is\n owed \u2014 tokens bought in earlier swaps and the fees its liquidity\n earned \u2014 into the wallet. Good to run after any trading session.\n"
212
+ );
213
+ }
214
+ async function main(argv) {
215
+ if (argv.includes("--help") || argv.includes("-h")) {
216
+ console.log(help(USAGE));
217
+ return;
218
+ }
219
+ try {
220
+ await setup(argv);
221
+ } catch (error) {
222
+ console.error("\nSETUP_FAILED:", error instanceof Error ? error.message : error);
223
+ process.exit(1);
224
+ }
225
+ }
226
+ export {
227
+ main
228
+ };
229
+ //# sourceMappingURL=setup-CZI3SHUT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/setup.ts"],"sourcesContent":["/**\n * Shield Swap account bootstrap — the startup gauntlet, idempotent.\n *\n * Run it as many times as needed; every step is check-then-act, so a failed\n * or interrupted run resumes where it stopped:\n *\n * 1. Key material — reuse the stored account, import the user's\n * existing key, or (only with --new) generate one\n * 2. DEX authentication — challenge/verify session with the account\n * 3. Provable API — reuse/import credentials, else self-register a\n * consumer for proving + scanning\n * 4. Invite code — check access; redeem a code when one is provided\n * 5. API token — mint a long-lived ss_ token for later sessions\n * 6. Airdrop — request testnet tokens when holdings are empty,\n * then poll until the PRIVATE records land\n *\n * Usage:\n * shield-swap setup --new # brand-new account\n * shield-swap setup --network mainnet --private-key-file <path> # mainnet\n * shield-swap setup --private-key-file <path> # returning user (key in a file)\n * shield-swap setup --invite-code CODE # when access is locked\n * shield-swap setup --api-url <origin> # pin a DEX API deployment\n *\n * A private key is NEVER pasted into a conversation or command history: a\n * returning user either writes it to a file and passes the path, or exports\n * SHIELD_SWAP_PRIVATE_KEY (or SHIELD_SWAP_PRIVATE_KEY_FILE) in their own\n * shell. Other environment fallbacks: ALEO_CONSUMER_ID + ALEO_DPS_API_KEY,\n * SHIELD_SWAP_INVITE_CODE, SHIELD_SWAP_API_URL.\n *\n * Exit codes: 0 ready · 2 needs input from the user (message says what) ·\n * 3 airdrop still pending · 1 anything else.\n *\n * State lands in ./.shield-swap/<network>/state.json (private key +\n * credentials — gitignore it, treat it like a wallet file). Nothing is shared\n * between networks, including the blinded identity store, whose reservations\n * are only meaningful against the chain they were checked on.\n *\n * Mainnet moves real value, so it is never the default: pass\n * `--network mainnet` explicitly. The airdrop step is testnet-only and refuses\n * to run on mainnet rather than pretending a faucet exists.\n */\nimport { readFileSync } from 'node:fs'\nimport { help } from '../color.js'\nimport { ApiError, DEFAULT_API_URL } from '@provablehq/shield-swap-sdk'\nimport { fileCredentialStore } from '@provablehq/veil-aleo-sdk/node'\nimport {\n loadState,\n saveState,\n ensureKeyMaterial,\n credentialsPath,\n resolveNetwork,\n stateDir,\n NeedsConfigDecisionError,\n loadSession,\n formatAmount,\n pollUntil,\n} from '../session.js'\n\nconst USAGE = `shield-swap setup — bootstrap an account and get it funded\n\n --new generate a brand-new account\n --private-key-file <path> import an existing key, read from this file\n --consumer-id <id> Provable API consumer id (else self-registers)\n --api-key <key> Provable API key\n --invite-code <code> redeem an invite code when access is locked\n --api-url <origin> pin a DEX API deployment\n --network <testnet|mainnet> default testnet\n\nEvery step is check-then-act, so re-running resumes where a failed run stopped.\n\nA private key is NEVER pasted into a conversation or command history: write it\nto a file and pass --private-key-file, or export SHIELD_SWAP_PRIVATE_KEY in your\nown shell. Other environment fallbacks: SHIELD_SWAP_PRIVATE_KEY_FILE,\nALEO_CONSUMER_ID, ALEO_DPS_API_KEY, SHIELD_SWAP_INVITE_CODE, SHIELD_SWAP_API_URL.\n\nExit codes: 0 ready · 2 needs input from the user · 3 airdrop still pending ·\n1 anything else.`\n\n/**\n * Reads a `--flag value` pair out of the arguments.\n *\n * `setup` keeps its own reader rather than using the shared `flags()`: every\n * option is an optional string with an environment-variable fallback, and none\n * of them gate a transaction, so the strict unknown-flag rejection that\n * protects the spending commands would only get in the way here.\n *\n * @param argv Arguments after the subcommand name.\n * @param flag The flag to look for, leading dashes included.\n * @returns The argument that follows the flag, or undefined when it is absent.\n */\nfunction argValue(argv: string[], flag: string): string | undefined {\n const i = argv.indexOf(flag)\n return i >= 0 ? argv[i + 1] : undefined\n}\n\n/**\n * Resolves the private key to import, when the caller is restoring an account.\n *\n * The key itself never travels through a conversation or the command line: it\n * is read from a file the user wrote, or from an env var the user exported in\n * their own shell.\n *\n * @param argv Arguments after the subcommand name.\n * @returns The key to import, or undefined when this is a fresh account.\n * @throws If the named key file exists but is empty.\n */\nfunction resolveImportKey(argv: string[]): string | undefined {\n const keyFile = argValue(argv, '--private-key-file') ?? process.env.SHIELD_SWAP_PRIVATE_KEY_FILE\n if (keyFile) {\n const key = readFileSync(keyFile, 'utf8').trim()\n if (!key) throw new Error(`private key file ${keyFile} is empty`)\n return key\n }\n return process.env.SHIELD_SWAP_PRIVATE_KEY\n}\n\n/**\n * Bootstraps the account, one idempotent step at a time.\n *\n * @param argv Arguments after the subcommand name.\n */\nasync function setup(argv: string[]): Promise<void> {\n const inviteCode = argValue(argv, '--invite-code') ?? process.env.SHIELD_SWAP_INVITE_CODE\n const consumerId = argValue(argv, '--consumer-id') ?? process.env.ALEO_CONSUMER_ID\n const apiKey = argValue(argv, '--api-key') ?? process.env.ALEO_DPS_API_KEY\n // Flag-only on purpose: SHIELD_SWAP_API_URL stays an ephemeral per-run\n // override (see loadSession); only an explicit --api-url pins the\n // deployment and resets deployment-scoped state.\n const network = resolveNetwork(argValue(argv, '--network'))\n const apiUrl = argValue(argv, '--api-url')?.replace(/\\/$/, '')\n const credentialStore = fileCredentialStore(credentialsPath(network))\n const allowGenerate = argv.includes('--new')\n const importKey = resolveImportKey(argv)\n\n // ── 1 + 2: key material and Provable API credentials ────────────────\n let state = loadState(network)\n console.log(`network: ${network} · state: ${stateDir(network)}`)\n\n // Pin the DEX API deployment. The access grant, API token, and airdrop\n // job all live in one deployment's database — switching deployments\n // invalidates them, so clear them and let the later steps re-derive.\n // Account key material and Provable API credentials are NOT touched\n // (chain- and prover-scoped, not DEX-scoped).\n if (apiUrl && apiUrl !== (state.apiUrl ?? DEFAULT_API_URL)) {\n state.apiUrl = apiUrl\n state.dexApiToken = undefined\n state.accessRedeemed = undefined\n state.airdropJobId = undefined\n saveState(state)\n console.log(`✓ DEX API pinned to ${state.apiUrl} (deployment-scoped state reset)`)\n }\n try {\n state = await ensureKeyMaterial(state, { importKey, allowGenerate })\n } catch (err) {\n if (err instanceof NeedsConfigDecisionError) {\n console.error(\n '\\nNEEDS_CONFIG_DECISION: no shield-swap account is configured here. Ask the user ' +\n 'whether they already have one before creating anything. NEVER ask them to paste ' +\n 'a private key into the conversation:\\n' +\n ' - existing account → the user saves their key to a file themselves, then re-run\\n' +\n ' with --private-key-file <path> (or they export SHIELD_SWAP_PRIVATE_KEY in\\n' +\n ' their own shell). Add --consumer-id/--api-key if they have Provable API\\n' +\n ' credentials.\\n' +\n ' - brand new → re-run with --new\\n',\n )\n process.exit(2)\n }\n throw err\n }\n console.log(`✓ account: ${state.address}`)\n\n // Supplied credentials win over registering a new consumer, so a returning\n // user keeps theirs. Absent both, the client registers one below. Awaited\n // because ProvableCredentialStore permits async: this store happens to be\n // synchronous, but reading a promise as a value would silently skip the seed\n // and leave the write unobserved.\n if (consumerId && apiKey && !(await credentialStore.load())) {\n await credentialStore.save({ consumerId, apiKey })\n }\n\n // Credentials used to live in the state file. Move them rather than letting\n // the client register a replacement: an API key is issued once and cannot be\n // reissued, so a fresh consumer would abandon the old one.\n const legacy = (state as { provableApi?: { consumerId: string; apiKey: string } }).provableApi\n if (legacy && !(await credentialStore.load())) {\n await credentialStore.save(legacy)\n delete (state as { provableApi?: unknown }).provableApi\n saveState(state)\n console.log(`✓ moved Provable API credentials to ${credentialsPath(network)}`)\n }\n\n // ── 3: wire the client and authenticate with the DEX API ────────────\n // The network must be passed explicitly: loadSession defaults to testnet, so\n // omitting it would authenticate, register, and redeem against testnet while\n // writing the results into the network-scoped state this script resolved.\n const { client, account } = await loadSession({ network })\n console.log('✓ DEX API session established (challenge/verify)')\n\n // Front-loaded on purpose: registration would otherwise happen on the first\n // prove or scan, and a newly issued API key is only reportable here.\n const provable = await client.authenticateProvableApi()\n console.log(\n `✓ Provable API consumer: ${provable.credentials.consumerId}` +\n (provable.registered ? ` (registered, saved to ${credentialsPath(network)})` : ''),\n )\n\n // ── 4: invite-code access gate ───────────────────────────────────────\n const status = await client.api.getAccessStatus()\n if (!status.has_access) {\n if (!inviteCode) {\n console.error(\n '\\nNEEDS_INVITE_CODE: this account has not redeemed an invite code, so ' +\n 'the DEX API is locked. Ask the user for their invite code, then re-run:\\n' +\n ' shield-swap setup --invite-code <code>\\n',\n )\n process.exit(2)\n }\n // Distributed codes come in two kinds with one purpose: access codes\n // (/access/redeem) and referral codes (/referral/redeem) both unlock\n // the account. Try both before rejecting the code.\n let redeemed = false\n for (const attempt of [\n () => client.api.redeemAccessCode(inviteCode),\n () => client.api.redeemReferralCode(inviteCode),\n ]) {\n try {\n await attempt()\n redeemed = true\n break\n } catch (err) {\n if (err instanceof ApiError && err.status === 400) continue\n throw err\n }\n }\n if (!redeemed) {\n console.error(\n `\\nINVALID_INVITE_CODE: the server rejected \"${inviteCode}\" as both an access ` +\n 'code and a referral code. Ask the user for a valid, unused code and re-run.\\n',\n )\n process.exit(2)\n }\n state.accessRedeemed = true\n saveState(state)\n console.log('✓ code redeemed — access unlocked')\n } else {\n state.accessRedeemed = true\n saveState(state)\n console.log('✓ access already granted')\n }\n\n // ── 5: long-lived API token for later sessions ───────────────────────\n if (!state.dexApiToken) {\n const created = await client.api.createApiToken({ name: `ss-agent-${account.address.slice(5, 17)}` })\n state.dexApiToken = created.token\n saveState(state)\n console.log(`✓ minted DEX API token (${created.token_prefix}…, stored in state file)`)\n } else {\n console.log('✓ DEX API token already on file')\n }\n\n // ── 6: airdrop when the account holds nothing ────────────────────────\n // The faucet delivers PRIVATE records, so the check must scan the private\n // side; a fresh account's public balances stay zero even after funding.\n const funded = async () => {\n const balances = await client.getBalances()\n return Object.values(balances).some((b) => b.total > 0n)\n }\n if (await funded()) {\n console.log('✓ account already funded')\n } else if (network === 'mainnet') {\n // There is no mainnet faucet, and quietly skipping would leave the account\n // configured but unable to trade — a failure a user would only discover\n // when a swap could not select a record.\n throw new Error(\n `account ${account.address} holds no tokens on mainnet, and there is no faucet to draw from. ` +\n 'Fund it from an exchange or another wallet, then re-run this script to verify.',\n )\n } else {\n // Request the faucet at most once per account: the job id persists in\n // the state file, so a re-run resumes polling instead of double-drawing.\n if (!state.airdropJobId) {\n const job = await client.api.airdrop(account.address)\n state.airdropJobId = job.job_id\n saveState(state)\n console.log(`… airdrop started (job ${job.job_id})`)\n } else {\n console.log(`… resuming airdrop job ${state.airdropJobId}`)\n }\n\n // Two phases: the faucet job finishing (fast), then the record service\n // indexing the new private records (slower, asynchronous).\n let job: Awaited<ReturnType<typeof client.api.getAirdropStatus>> | null = null\n const jobDone = await pollUntil(async () => {\n job = await client.api.getAirdropStatus(state.airdropJobId!).catch(() => null)\n return job?.status === 'complete'\n }, 24, 5_000)\n if (!jobDone) {\n console.error(\n `\\nAIRDROP_PENDING: faucet job ${state.airdropJobId} has not completed yet ` +\n `(last status: ${job ? (job as { status?: string }).status : 'unknown'}). ` +\n 'Re-run `shield-swap setup` in a few minutes — it resumes this job, it does not double-request.\\n',\n )\n process.exit(3)\n }\n const rejected = (job!.results ?? []).filter((r: { status?: string }) => r.status !== 'accepted')\n if (rejected.length > 0) {\n // The job finished but some transfers failed — surface it and allow a\n // fresh request next run instead of resuming a dead job forever.\n state.airdropJobId = undefined\n saveState(state)\n console.error(\n `\\nAIRDROP_FAILED: faucet job finished with rejected transfers: ` +\n `${rejected.map((r: { symbol?: string; status?: string }) => `${r.symbol}:${r.status}`).join(', ')}. ` +\n 'Re-run `shield-swap setup` to request a fresh airdrop.\\n',\n )\n process.exit(3)\n }\n console.log('… faucet job complete — waiting for the records to become scannable')\n\n const landed = await pollUntil(funded, 36, 10_000)\n if (!landed) {\n console.error(\n '\\nAIRDROP_PENDING: the faucet finished but the records are not scannable ' +\n 'yet (the record service indexes asynchronously). Re-run `shield-swap setup` in a ' +\n 'few minutes — it resumes this job, it does not double-request.\\n',\n )\n process.exit(3)\n }\n console.log('✓ airdrop landed')\n }\n\n // ── report ────────────────────────────────────────────────────────────\n const balances = await client.getBalances()\n console.log(`\\nAccount ${account.address} is ready on ${network}:`)\n for (const entry of Object.values(balances)) {\n if (entry.total > 0n) {\n const priv = formatAmount(entry.private, entry.decimals, entry.symbol)\n const pub = formatAmount(entry.public, entry.decimals, entry.symbol)\n console.log(` ${entry.symbol}: ${priv} private, ${pub} public`)\n }\n }\n console.log(\n '\\nASK_NEXT_ACTION: setup is complete — ask the user what to do next with ONE ' +\n 'user-selectable prompt (the harness\\'s selection UI if it has one, a numbered ' +\n 'list otherwise) offering ALL SEVEN options below, in this order. Free-form ' +\n 'input stays available as the escape hatch (\"Other\"); map whatever the user ' +\n 'types onto the runbooks before improvising against the SDK. Do not pick for ' +\n 'them. Frame the setting first: Shield Swap is a private exchange on Aleo — ' +\n 'what is traded, and by whom, stays hidden on the public chain. ' +\n (network === 'mainnet'\n ? 'This account is on MAINNET: every trade below moves real funds, and there ' +\n 'is no faucet to recover from a mistake. Say so before the user picks.'\n : 'This account is on the test network, so trading uses test tokens.') +\n '\\n\\n' +\n '1. Develop on Shield Swap (developing.md). For a user building their own\\n' +\n ' dApp, trading bot, or server/agent integration rather than trading\\n' +\n ' here. If chosen, FIRST ask what they are building, then follow\\n' +\n ' developing.md — it picks the packages by where their keys live and\\n' +\n ' maps to the docs, examples, and integration caveats.\\n' +\n '2. Follow their own playbook. Ask whether they have instructions of\\n' +\n ' their own — a markdown strategy file, notes, or a memory store such\\n' +\n ' as an Obsidian vault. Their document decides WHAT to do; the runbooks\\n' +\n ' here describe HOW each step works.\\n' +\n '3. Swap tokens (swapping.md). Trade one token for another. It settles\\n' +\n ' in two steps — placing the trade, then collecting what was bought —\\n' +\n ' and both happen in one go. The natural first move for a trader.\\n' +\n '4. Several swaps at once (swapping.md, concurrency recipe). Place a\\n' +\n ' handful of trades in parallel and watch them all land — the busiest\\n' +\n ' way to exercise the exchange. First show the user which trades are\\n' +\n ' possible right now and ask how many (and which) they want; collect\\n' +\n ' each one as it lands.\\n' +\n '5. Open a liquidity position (liquidity.md). Instead of trading, become\\n' +\n ' the market: deposit a pair of tokens so other people can trade against\\n' +\n ' them. The user picks the price range their deposit works in, and while\\n' +\n ' the market price sits inside that range they earn a small cut of every\\n' +\n ' trade that passes through.\\n' +\n '6. Add or remove liquidity (liquidity.md). Top up a position, or take\\n' +\n ' some of it back out — whatever comes out becomes earnings to collect.\\n' +\n '7. Collect earnings (collecting.md). Sweep up everything the account is\\n' +\n ' owed — tokens bought in earlier swaps and the fees its liquidity\\n' +\n ' earned — into the wallet. Good to run after any trading session.\\n',\n )\n}\n\n\n/**\n * Runs the `setup` subcommand.\n *\n * Failures are reported as `SETUP_FAILED: <message>` with exit 1, which\n * startup.md documents as the signal to read the message and retry once\n * before digging — the step-specific exits (`NEEDS_CONFIG_DECISION`,\n * `NEEDS_INVITE_CODE`) leave from inside {@link setup} with their own codes.\n *\n * @param argv Arguments after the subcommand name, as the dispatcher supplies them.\n */\nexport async function main(argv: string[]): Promise<void> {\n // Checked before anything else: setup's first act is to read and migrate the\n // state file, so a --help that fell through would bootstrap the account it\n // was only asked to describe.\n if (argv.includes('--help') || argv.includes('-h')) {\n console.log(help(USAGE))\n return\n }\n try {\n await setup(argv)\n } catch (error) {\n console.error('\\nSETUP_FAILED:', error instanceof Error ? error.message : error)\n process.exit(1)\n }\n}\n"],"mappings":"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","names":["balances","job"]}
@@ -0,0 +1,143 @@
1
+ import {
2
+ basisPoints,
3
+ confirmed,
4
+ done,
5
+ fail,
6
+ flags,
7
+ output,
8
+ run,
9
+ step,
10
+ warn
11
+ } from "./chunk-IBVZHLUT.js";
12
+ import "./chunk-IHYFMX5A.js";
13
+ import {
14
+ formatAmount,
15
+ loadSession
16
+ } from "./chunk-2OT6LZPW.js";
17
+
18
+ // src/commands/swap.ts
19
+ import { SwapOutputNotFinalizedError, parseUnits } from "@provablehq/shield-swap-sdk";
20
+ var USAGE = `shield-swap swap \u2014 sell one token for another and claim the output
21
+
22
+ --from <symbol|id> token to sell (required)
23
+ --to <symbol|id> token to buy (required)
24
+ --amount <decimal> human amount, e.g. 1.5 (or --amount-raw)
25
+ --amount-raw <integer> raw base units
26
+ --slippage <bps> default 50 (0.5%)
27
+ --no-claim submit the swap, leave the output for shield-swap history
28
+ --network <testnet|mainnet> default testnet
29
+ --execute actually submit
30
+ --json machine-readable output`;
31
+ async function main(argv) {
32
+ const args = flags(
33
+ {
34
+ from: { type: "string" },
35
+ to: { type: "string" },
36
+ amount: { type: "string" },
37
+ "amount-raw": { type: "string" },
38
+ slippage: { type: "string" },
39
+ "no-claim": { type: "boolean" }
40
+ },
41
+ USAGE,
42
+ argv
43
+ );
44
+ if (!args.from || !args.to) fail(`--from and --to are required.
45
+
46
+ ${USAGE}`);
47
+ if (!args.amount && !args["amount-raw"]) fail(`--amount or --amount-raw is required.
48
+
49
+ ${USAGE}`);
50
+ const slippageBps = basisPoints(args.slippage, "--slippage");
51
+ await run(async () => {
52
+ const { client, network } = await loadSession({ network: args.network });
53
+ done(`session on ${network}`);
54
+ const from = await client.tokenData(args.from);
55
+ const amountIn = args["amount-raw"] ? BigInt(args["amount-raw"]) : parseUnits(args.amount, from.decimals);
56
+ const balances = await client.getBalances({ tokens: [from.id] });
57
+ const held = balances[from.id]?.private ?? 0n;
58
+ if (held < amountIn) {
59
+ throw new Error(
60
+ `holding ${formatAmount(held, from.decimals, from.symbol)} privately, which is less than the ${formatAmount(amountIn, from.decimals, from.symbol)} this swap sells. Note a swap spends one record, not the sum of several.`
61
+ );
62
+ }
63
+ step(`planning ${from.symbol} \u2192 ${args.to}`);
64
+ const plan = await client.planSwap({
65
+ from: from.id,
66
+ to: args.to,
67
+ amountIn,
68
+ ...slippageBps === void 0 ? {} : { slippageBps }
69
+ });
70
+ done(`${plan.multiHop ? `${plan.poolKeys.length}-hop route` : "direct pool"} found`);
71
+ const planLines = [
72
+ ["sell", formatAmount(plan.amountIn, plan.from.decimals, plan.from.symbol)],
73
+ [
74
+ "buy",
75
+ plan.expectedOut > 0n ? formatAmount(plan.expectedOut, plan.to.decimals, plan.to.symbol) : `${plan.to.symbol} (no quote available)`
76
+ ],
77
+ [
78
+ "floor",
79
+ plan.minOut > 0n ? formatAmount(plan.minOut, plan.to.decimals, plan.to.symbol) : "none \u2014 an unquoted swap accepts any fill"
80
+ ],
81
+ ["route", plan.poolKeys.join(" \u2192 ")],
82
+ ["claim", args["no-claim"] ? "no, left for `shield-swap history`" : "yes, in this run"]
83
+ ];
84
+ if (!confirmed({ execute: args.execute, network, plan: planLines })) {
85
+ output({ network, submitted: false, plan: { ...plan, imports: Object.keys(plan.imports) } }, () => {
86
+ });
87
+ return;
88
+ }
89
+ step("proving and submitting the swap \u2014 this takes a minute or two");
90
+ const handle = plan.multiHop ? await client.swapMultiHop({
91
+ poolKeys: plan.poolKeys,
92
+ tokenInId: plan.from.id,
93
+ amountIn: plan.amountIn,
94
+ ...plan.expectedOut > 0n ? { expectedOut: plan.expectedOut } : {},
95
+ slippageBps: plan.slippageBps,
96
+ imports: plan.imports
97
+ }) : await client.swap({
98
+ poolKey: plan.poolKeys[0],
99
+ tokenInId: plan.from.id,
100
+ amountIn: plan.amountIn,
101
+ ...plan.expectedOut > 0n ? { expectedOut: plan.expectedOut } : {},
102
+ slippageBps: plan.slippageBps,
103
+ imports: plan.imports
104
+ });
105
+ done(`swap landed: tx ${handle.transactionId}, swapId ${handle.swapId}`);
106
+ let claim;
107
+ if (!args["no-claim"]) {
108
+ for (let attempt = 0; attempt < 20 && !claim; attempt++) {
109
+ try {
110
+ step(`claiming the output (attempt ${attempt + 1})`);
111
+ claim = await client.claimSwapOutput({ handle, imports: plan.imports });
112
+ } catch (error) {
113
+ if (!(error instanceof SwapOutputNotFinalizedError)) throw error;
114
+ await new Promise((resolve) => setTimeout(resolve, 15e3));
115
+ }
116
+ }
117
+ if (claim) done(`claimed ${formatAmount(claim.amountOut, plan.to.decimals, plan.to.symbol)} (tx ${claim.transactionId})`);
118
+ else warn("the output has not finalized yet \u2014 claim it later with `shield-swap history --claim --execute`");
119
+ }
120
+ output(
121
+ {
122
+ network,
123
+ submitted: true,
124
+ swapId: handle.swapId,
125
+ transactionId: handle.transactionId,
126
+ sold: plan.amountIn,
127
+ bought: claim?.amountOut ?? null,
128
+ claimTransactionId: claim?.transactionId ?? null
129
+ },
130
+ (data) => {
131
+ console.log(`
132
+ ${formatAmount(data.sold, plan.from.decimals, plan.from.symbol)} sold.`);
133
+ if (data.bought !== null) {
134
+ console.log(`${formatAmount(data.bought, plan.to.decimals, plan.to.symbol)} received.`);
135
+ } else console.log(`Output not claimed yet \u2014 swapId ${data.swapId}`);
136
+ }
137
+ );
138
+ });
139
+ }
140
+ export {
141
+ main
142
+ };
143
+ //# sourceMappingURL=swap-W72XGG7Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/swap.ts"],"sourcesContent":["/**\n * Swap — sell one token for another, single hop or routed, then claim.\n *\n * A private swap is two transactions: the request, then a claim that turns the\n * output into records the account holds. This does both, because leaving the\n * claim for later is how proceeds get forgotten.\n *\n * `planSwap` picks the route from the API and checks every hop on chain before\n * anything is submitted, so the plan printed below is the plan that executes.\n * The blinded identity is reserved and recorded automatically — nothing to track.\n *\n * SPENDS REAL FUNDS with --execute. Without it, prints the plan and stops.\n *\n * Usage:\n * shield-swap swap --from USDCx --to ETH --amount 1.5\n * shield-swap swap --from USDCx --to ETH --amount 1.5 --execute\n * shield-swap swap --from USDCx --to ALEO --amount 1.5 --slippage 100 --execute\n * shield-swap swap --network mainnet --from USDCx --to ETH --amount 5 --execute\n * shield-swap swap --from USDCx --to ETH --amount-raw 1500000 --execute\n * shield-swap swap --from USDCx --to ETH --amount 1.5 --no-claim --execute\n */\nimport { SwapOutputNotFinalizedError, parseUnits } from '@provablehq/shield-swap-sdk'\nimport { loadSession, formatAmount } from '../session.js'\nimport { flags, step, done, warn, output, confirmed, run, fail, basisPoints } from '../shared.js'\n\nconst USAGE = `shield-swap swap — sell one token for another and claim the output\n\n --from <symbol|id> token to sell (required)\n --to <symbol|id> token to buy (required)\n --amount <decimal> human amount, e.g. 1.5 (or --amount-raw)\n --amount-raw <integer> raw base units\n --slippage <bps> default 50 (0.5%)\n --no-claim submit the swap, leave the output for shield-swap history\n --network <testnet|mainnet> default testnet\n --execute actually submit\n --json machine-readable output`\n\n/**\n * Runs the `swap` subcommand.\n *\n * @param argv Arguments after the subcommand name, as the dispatcher supplies them.\n */\nexport async function main(argv: string[]): Promise<void> {\n const args = flags(\n {\n from: { type: 'string' },\n to: { type: 'string' },\n amount: { type: 'string' },\n 'amount-raw': { type: 'string' },\n slippage: { type: 'string' },\n 'no-claim': { type: 'boolean' },\n },\n USAGE,\n argv,\n )\n\n if (!args.from || !args.to) fail(`--from and --to are required.\\n\\n${USAGE}`)\n if (!args.amount && !args['amount-raw']) fail(`--amount or --amount-raw is required.\\n\\n${USAGE}`)\n\n // Validated before the session is built, so a bad flag costs no network calls.\n const slippageBps = basisPoints(args.slippage as string | undefined, '--slippage')\n\n await run(async () => {\n const { client, network } = await loadSession({ network: args.network as string | undefined })\n done(`session on ${network}`)\n\n const from = await client.tokenData(args.from as string)\n const amountIn = args['amount-raw']\n ? BigInt(args['amount-raw'] as string)\n : parseUnits(args.amount as string, from.decimals)\n\n // Fail before planning if the account cannot cover it: the private side is\n // what funds a swap, and record selection needs ONE record big enough.\n const balances = await client.getBalances({ tokens: [from.id] })\n const held = balances[from.id]?.private ?? 0n\n if (held < amountIn) {\n throw new Error(\n `holding ${formatAmount(held, from.decimals, from.symbol)} privately, ` +\n `which is less than the ${formatAmount(amountIn, from.decimals, from.symbol)} this swap sells. ` +\n 'Note a swap spends one record, not the sum of several.',\n )\n }\n\n step(`planning ${from.symbol} → ${args.to as string}`)\n const plan = await client.planSwap({\n from: from.id,\n to: args.to as string,\n amountIn,\n ...(slippageBps === undefined ? {} : { slippageBps }),\n })\n done(`${plan.multiHop ? `${plan.poolKeys.length}-hop route` : 'direct pool'} found`)\n\n const planLines: Array<readonly [string, string]> = [\n ['sell', formatAmount(plan.amountIn, plan.from.decimals, plan.from.symbol)],\n [\n 'buy',\n plan.expectedOut > 0n\n ? formatAmount(plan.expectedOut, plan.to.decimals, plan.to.symbol)\n : `${plan.to.symbol} (no quote available)`,\n ],\n [\n 'floor',\n plan.minOut > 0n\n ? formatAmount(plan.minOut, plan.to.decimals, plan.to.symbol)\n : 'none — an unquoted swap accepts any fill',\n ],\n ['route', plan.poolKeys.join(' → ')],\n ['claim', args['no-claim'] ? 'no, left for `shield-swap history`' : 'yes, in this run'],\n ]\n if (!confirmed({ execute: args.execute as boolean | undefined, network, plan: planLines })) {\n output({ network, submitted: false, plan: { ...plan, imports: Object.keys(plan.imports) } }, () => {})\n return\n }\n\n step('proving and submitting the swap — this takes a minute or two')\n const handle = plan.multiHop\n ? await client.swapMultiHop({\n poolKeys: plan.poolKeys,\n tokenInId: plan.from.id,\n amountIn: plan.amountIn,\n ...(plan.expectedOut > 0n ? { expectedOut: plan.expectedOut } : {}),\n slippageBps: plan.slippageBps,\n imports: plan.imports,\n })\n : await client.swap({\n poolKey: plan.poolKeys[0]!,\n tokenInId: plan.from.id,\n amountIn: plan.amountIn,\n ...(plan.expectedOut > 0n ? { expectedOut: plan.expectedOut } : {}),\n slippageBps: plan.slippageBps,\n imports: plan.imports,\n })\n done(`swap landed: tx ${handle.transactionId}, swapId ${handle.swapId}`)\n\n let claim: { transactionId: string; amountOut: bigint } | undefined\n if (!args['no-claim']) {\n // The output becomes claimable a few blocks after the swap finalizes, so the\n // first attempts failing is the normal path rather than an error.\n for (let attempt = 0; attempt < 20 && !claim; attempt++) {\n try {\n step(`claiming the output (attempt ${attempt + 1})`)\n claim = await client.claimSwapOutput({ handle, imports: plan.imports })\n } catch (error) {\n if (!(error instanceof SwapOutputNotFinalizedError)) throw error\n await new Promise((resolve) => setTimeout(resolve, 15_000))\n }\n }\n if (claim) done(`claimed ${formatAmount(claim.amountOut, plan.to.decimals, plan.to.symbol)} (tx ${claim.transactionId})`)\n else warn('the output has not finalized yet — claim it later with `shield-swap history --claim --execute`')\n }\n\n output(\n {\n network,\n submitted: true,\n swapId: handle.swapId,\n transactionId: handle.transactionId,\n sold: plan.amountIn,\n bought: claim?.amountOut ?? null,\n claimTransactionId: claim?.transactionId ?? null,\n },\n (data) => {\n // Raw base units stay in the JSON, where a caller needs them exact; a\n // person reading `200000000000000 ETH` has to count 18 digits to learn it\n // was 0.0002, so the human lines carry the token's decimals.\n console.log(`\\n${formatAmount(data.sold, plan.from.decimals, plan.from.symbol)} sold.`)\n if (data.bought !== null) {\n console.log(`${formatAmount(data.bought, plan.to.decimals, plan.to.symbol)} received.`)\n } else console.log(`Output not claimed yet — swapId ${data.swapId}`)\n },\n )\n })\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,169 @@
1
+ import {
2
+ basisPoints,
3
+ confirmed,
4
+ done,
5
+ fail,
6
+ flags,
7
+ output,
8
+ run,
9
+ step,
10
+ warn
11
+ } from "./chunk-IBVZHLUT.js";
12
+ import "./chunk-IHYFMX5A.js";
13
+ import {
14
+ formatAmount,
15
+ loadSession
16
+ } from "./chunk-2OT6LZPW.js";
17
+
18
+ // src/commands/swap-concurrent.ts
19
+ import { SwapOutputNotFinalizedError, parseUnits } from "@provablehq/shield-swap-sdk";
20
+ var USAGE = `shield-swap swap-concurrent \u2014 run several swaps at once
21
+
22
+ --swap <from:to:amount> repeatable, e.g. --swap USDCx:ETH:0.5
23
+ --slippage <bps> default 50 (0.5%)
24
+ --no-claim leave the outputs for shield-swap history
25
+ --network <testnet|mainnet> default testnet
26
+ --execute actually submit
27
+ --json machine-readable output
28
+
29
+ Each --swap must sell a DIFFERENT token: concurrent swaps selling the same token
30
+ can select the same record and one will fail as a double-spend.`;
31
+ async function main(argv) {
32
+ const args = flags(
33
+ {
34
+ swap: { type: "string", multiple: true },
35
+ slippage: { type: "string" },
36
+ "no-claim": { type: "boolean" }
37
+ },
38
+ USAGE,
39
+ argv
40
+ );
41
+ const specs = args.swap ?? [];
42
+ if (specs.length < 2) fail(`pass at least two --swap arguments.
43
+
44
+ ${USAGE}`);
45
+ const slippageBps = basisPoints(args.slippage, "--slippage");
46
+ await run(async () => {
47
+ const { client, network } = await loadSession({ network: args.network });
48
+ done(`session on ${network}`);
49
+ const legs = [];
50
+ for (const spec of specs) {
51
+ const [from, to, amount] = spec.split(":");
52
+ if (!from || !to || !amount) {
53
+ throw new Error(`"${spec}" is not from:to:amount, e.g. USDCx:ETH:0.5`);
54
+ }
55
+ const token = await client.tokenData(from);
56
+ step(`planning ${from} \u2192 ${to}`);
57
+ const plan = await client.planSwap({
58
+ from: token.id,
59
+ to,
60
+ amountIn: parseUnits(amount, token.decimals),
61
+ ...slippageBps === void 0 ? {} : { slippageBps }
62
+ });
63
+ legs.push(plan);
64
+ }
65
+ const sellers = legs.map((leg) => leg.from.id);
66
+ const duplicated = sellers.filter((id, index) => sellers.indexOf(id) !== index);
67
+ if (duplicated.length) {
68
+ const symbol = legs.find((leg) => leg.from.id === duplicated[0]).from.symbol;
69
+ throw new Error(
70
+ `two legs both sell ${symbol}. Record selection picks one record per swap, so these would contend for the same record and one would fail as a double-spend. Sell a different token in each leg, or run them sequentially.`
71
+ );
72
+ }
73
+ const balances = await client.getBalances();
74
+ for (const leg of legs) {
75
+ const held = balances[leg.from.id]?.private ?? 0n;
76
+ if (held < leg.amountIn) {
77
+ throw new Error(
78
+ `holding ${formatAmount(held, leg.from.decimals, leg.from.symbol)} privately, less than the ${formatAmount(leg.amountIn, leg.from.decimals, leg.from.symbol)} one leg sells.`
79
+ );
80
+ }
81
+ }
82
+ const planLines = legs.map(
83
+ (leg, i) => [
84
+ `swap ${i + 1}`,
85
+ `${formatAmount(leg.amountIn, leg.from.decimals, leg.from.symbol)} \u2192 ${leg.expectedOut > 0n ? formatAmount(leg.expectedOut, leg.to.decimals, leg.to.symbol) : `${leg.to.symbol} (no quote)`}${leg.multiHop ? ` via ${leg.poolKeys.length} hops` : ""}`
86
+ ]
87
+ );
88
+ if (!confirmed({
89
+ execute: args.execute,
90
+ network,
91
+ plan: [...planLines, ["submit", `${legs.length} swaps together`]]
92
+ })) {
93
+ output({ network, submitted: false, legs: legs.length }, () => {
94
+ });
95
+ return;
96
+ }
97
+ step(`submitting ${legs.length} swaps together`);
98
+ const submitted = await Promise.allSettled(
99
+ legs.map(
100
+ (leg) => leg.multiHop ? client.swapMultiHop({
101
+ poolKeys: leg.poolKeys,
102
+ tokenInId: leg.from.id,
103
+ amountIn: leg.amountIn,
104
+ ...leg.expectedOut > 0n ? { expectedOut: leg.expectedOut } : {},
105
+ slippageBps: leg.slippageBps,
106
+ imports: leg.imports
107
+ }) : client.swap({
108
+ poolKey: leg.poolKeys[0],
109
+ tokenInId: leg.from.id,
110
+ amountIn: leg.amountIn,
111
+ ...leg.expectedOut > 0n ? { expectedOut: leg.expectedOut } : {},
112
+ slippageBps: leg.slippageBps,
113
+ imports: leg.imports
114
+ })
115
+ )
116
+ );
117
+ const results = submitted.map((result, index) => {
118
+ const leg = legs[index];
119
+ if (result.status === "rejected") {
120
+ warn(`${leg.from.symbol} \u2192 ${leg.to.symbol} failed: ${result.reason.message}`);
121
+ return { pair: `${leg.from.symbol}\u2192${leg.to.symbol}`, ok: false, error: result.reason.message };
122
+ }
123
+ done(`${leg.from.symbol} \u2192 ${leg.to.symbol} landed: tx ${result.value.transactionId}`);
124
+ return { pair: `${leg.from.symbol}\u2192${leg.to.symbol}`, ok: true, handle: result.value, claimed: null };
125
+ });
126
+ if (!args["no-claim"]) {
127
+ for (const [index, result] of results.entries()) {
128
+ if (!result.ok) continue;
129
+ const leg = legs[index];
130
+ for (let attempt = 0; attempt < 20; attempt++) {
131
+ try {
132
+ step(`claiming ${result.pair} (attempt ${attempt + 1})`);
133
+ const claim = await client.claimSwapOutput({ handle: result.handle, imports: leg.imports });
134
+ result.claimed = claim.amountOut;
135
+ done(`claimed ${formatAmount(claim.amountOut, leg.to.decimals, leg.to.symbol)}`);
136
+ break;
137
+ } catch (error) {
138
+ if (!(error instanceof SwapOutputNotFinalizedError)) throw error;
139
+ await new Promise((resolve) => setTimeout(resolve, 15e3));
140
+ }
141
+ }
142
+ }
143
+ }
144
+ output(
145
+ {
146
+ network,
147
+ submitted: true,
148
+ results: results.map((result) => ({
149
+ pair: result.pair,
150
+ ok: result.ok,
151
+ ...result.ok ? { swapId: result.handle.swapId, transactionId: result.handle.transactionId, claimed: result.claimed } : { error: result.error }
152
+ }))
153
+ },
154
+ (data) => {
155
+ const landed = data.results.filter((result) => result.ok).length;
156
+ console.log(`
157
+ ${landed}/${data.results.length} swaps landed.`);
158
+ const unclaimed = data.results.filter((result) => "claimed" in result && result.claimed === null);
159
+ if (unclaimed.length) {
160
+ console.log(`${unclaimed.length} output(s) still unclaimed \u2014 \`shield-swap history --claim --execute\`.`);
161
+ }
162
+ }
163
+ );
164
+ });
165
+ }
166
+ export {
167
+ main
168
+ };
169
+ //# sourceMappingURL=swap-concurrent-IHGWJMST.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/swap-concurrent.ts"],"sourcesContent":["/**\n * Concurrent swaps — several trades in flight at once.\n *\n * Two things contend when swaps run in parallel, and only one is handled for you:\n *\n * Blinded identities are safe. Each swap reserves its own from the store the\n * session configures, and reservations serialize, so two swaps cannot derive\n * the same address and have the second revert on the uniqueness assert.\n *\n * Records are not. Selection picks ONE private record big enough for the\n * amount, so two swaps selling the same token can pick the same record and one\n * fails as a double-spend. This script therefore refuses to run two swaps that\n * sell the same token, rather than letting the chain reject them.\n *\n * Every swap is planned before any is submitted, so a bad leg is caught while\n * nothing has been spent.\n *\n * SPENDS REAL FUNDS with --execute.\n *\n * Usage:\n * shield-swap swap-concurrent --swap USDCx:ETH:0.5 --swap ALEO:ETH:1\n * shield-swap swap-concurrent --swap USDCx:ETH:0.5 --swap ALEO:ETH:1 --execute\n * shield-swap swap-concurrent --swap USDCx:ETH:0.5 --swap ALEO:ETH:1 --no-claim --execute\n */\nimport { SwapOutputNotFinalizedError, parseUnits } from '@provablehq/shield-swap-sdk'\nimport type { SwapPlan } from '@provablehq/shield-swap-sdk'\nimport { loadSession, formatAmount } from '../session.js'\nimport { flags, step, done, warn, output, confirmed, run, fail, basisPoints } from '../shared.js'\n\nconst USAGE = `shield-swap swap-concurrent — run several swaps at once\n\n --swap <from:to:amount> repeatable, e.g. --swap USDCx:ETH:0.5\n --slippage <bps> default 50 (0.5%)\n --no-claim leave the outputs for shield-swap history\n --network <testnet|mainnet> default testnet\n --execute actually submit\n --json machine-readable output\n\nEach --swap must sell a DIFFERENT token: concurrent swaps selling the same token\ncan select the same record and one will fail as a double-spend.`\n\n/**\n * Runs the `swap-concurrent` subcommand.\n *\n * @param argv Arguments after the subcommand name, as the dispatcher supplies them.\n */\nexport async function main(argv: string[]): Promise<void> {\n const args = flags(\n {\n swap: { type: 'string', multiple: true },\n slippage: { type: 'string' },\n 'no-claim': { type: 'boolean' },\n },\n USAGE,\n argv,\n )\n\n const specs = (args.swap as string[] | undefined) ?? []\n if (specs.length < 2) fail(`pass at least two --swap arguments.\\n\\n${USAGE}`)\n\n // Validated before the session is built, so a bad flag costs no network calls.\n const slippageBps = basisPoints(args.slippage as string | undefined, '--slippage')\n\n await run(async () => {\n const { client, network } = await loadSession({ network: args.network as string | undefined })\n done(`session on ${network}`)\n\n // Parse and plan every leg first. A malformed or unroutable leg should surface\n // before anything is submitted, not after half the batch has spent.\n const legs: SwapPlan[] = []\n for (const spec of specs) {\n const [from, to, amount] = spec.split(':')\n if (!from || !to || !amount) {\n throw new Error(`\"${spec}\" is not from:to:amount, e.g. USDCx:ETH:0.5`)\n }\n const token = await client.tokenData(from)\n step(`planning ${from} → ${to}`)\n const plan = await client.planSwap({\n from: token.id,\n to,\n amountIn: parseUnits(amount, token.decimals),\n ...(slippageBps === undefined ? {} : { slippageBps }),\n })\n legs.push(plan)\n }\n\n // The one hazard the store does not cover.\n const sellers = legs.map((leg) => leg.from.id)\n const duplicated = sellers.filter((id, index) => sellers.indexOf(id) !== index)\n if (duplicated.length) {\n const symbol = legs.find((leg) => leg.from.id === duplicated[0])!.from.symbol\n throw new Error(\n `two legs both sell ${symbol}. Record selection picks one record per swap, so these would ` +\n 'contend for the same record and one would fail as a double-spend. Sell a different token ' +\n 'in each leg, or run them sequentially.',\n )\n }\n\n // Enough of each token, checked against the private side that funds swaps.\n const balances = await client.getBalances()\n for (const leg of legs) {\n const held = balances[leg.from.id]?.private ?? 0n\n if (held < leg.amountIn) {\n throw new Error(\n `holding ${formatAmount(held, leg.from.decimals, leg.from.symbol)} privately, less than the ` +\n `${formatAmount(leg.amountIn, leg.from.decimals, leg.from.symbol)} one leg sells.`,\n )\n }\n }\n\n const planLines: Array<readonly [string, string]> = legs.map(\n (leg, i) =>\n [\n `swap ${i + 1}`,\n `${formatAmount(leg.amountIn, leg.from.decimals, leg.from.symbol)} → ` +\n `${leg.expectedOut > 0n ? formatAmount(leg.expectedOut, leg.to.decimals, leg.to.symbol) : `${leg.to.symbol} (no quote)`}` +\n `${leg.multiHop ? ` via ${leg.poolKeys.length} hops` : ''}`,\n ] as const,\n )\n if (\n !confirmed({\n execute: args.execute as boolean | undefined,\n network,\n plan: [...planLines, ['submit', `${legs.length} swaps together`] as const],\n })\n ) {\n output({ network, submitted: false, legs: legs.length }, () => {})\n return\n }\n\n step(`submitting ${legs.length} swaps together`)\n // allSettled, not all: one rejection must not abandon the swaps that landed,\n // because their outputs are claimable and would otherwise be forgotten.\n const submitted = await Promise.allSettled(\n legs.map((leg) =>\n leg.multiHop\n ? client.swapMultiHop({\n poolKeys: leg.poolKeys,\n tokenInId: leg.from.id,\n amountIn: leg.amountIn,\n ...(leg.expectedOut > 0n ? { expectedOut: leg.expectedOut } : {}),\n slippageBps: leg.slippageBps,\n imports: leg.imports,\n })\n : client.swap({\n poolKey: leg.poolKeys[0]!,\n tokenInId: leg.from.id,\n amountIn: leg.amountIn,\n ...(leg.expectedOut > 0n ? { expectedOut: leg.expectedOut } : {}),\n slippageBps: leg.slippageBps,\n imports: leg.imports,\n }),\n ),\n )\n\n const results = submitted.map((result, index) => {\n const leg = legs[index]!\n if (result.status === 'rejected') {\n warn(`${leg.from.symbol} → ${leg.to.symbol} failed: ${(result.reason as Error).message}`)\n return { pair: `${leg.from.symbol}→${leg.to.symbol}`, ok: false as const, error: (result.reason as Error).message }\n }\n done(`${leg.from.symbol} → ${leg.to.symbol} landed: tx ${result.value.transactionId}`)\n return { pair: `${leg.from.symbol}→${leg.to.symbol}`, ok: true as const, handle: result.value, claimed: null as bigint | null }\n })\n\n if (!args['no-claim']) {\n for (const [index, result] of results.entries()) {\n if (!result.ok) continue\n const leg = legs[index]!\n for (let attempt = 0; attempt < 20; attempt++) {\n try {\n step(`claiming ${result.pair} (attempt ${attempt + 1})`)\n const claim = await client.claimSwapOutput({ handle: result.handle, imports: leg.imports })\n result.claimed = claim.amountOut\n done(`claimed ${formatAmount(claim.amountOut, leg.to.decimals, leg.to.symbol)}`)\n break\n } catch (error) {\n if (!(error instanceof SwapOutputNotFinalizedError)) throw error\n await new Promise((resolve) => setTimeout(resolve, 15_000))\n }\n }\n }\n }\n\n output(\n {\n network,\n submitted: true,\n results: results.map((result) => ({\n pair: result.pair,\n ok: result.ok,\n ...(result.ok\n ? { swapId: result.handle.swapId, transactionId: result.handle.transactionId, claimed: result.claimed }\n : { error: result.error }),\n })),\n },\n (data) => {\n const landed = data.results.filter((result) => result.ok).length\n console.log(`\\n${landed}/${data.results.length} swaps landed.`)\n const unclaimed = data.results.filter((result) => 'claimed' in result && result.claimed === null)\n if (unclaimed.length) {\n console.log(`${unclaimed.length} output(s) still unclaimed — \\`shield-swap history --claim --execute\\`.`)\n }\n },\n )\n })\n}\n"],"mappings":"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","names":[]}