@caatinga/cli 2.1.0 → 2.2.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.
- package/README.md +12 -6
- package/dist/index.js +195 -13
- package/package.json +3 -3
- package/templates/marketplace-with-token/caatinga.template.json +1 -1
- package/templates/marketplace-with-token/package.json +3 -3
- package/templates/react-vite-counter/caatinga.template.json +1 -1
- package/templates/react-vite-counter/package.json +7 -7
- package/templates/react-vite-counter/pnpm-workspace.yaml +4 -10
- package/templates/react-vite-counter/src/App.tsx +17 -3
- package/templates/react-vite-counter/src/components/ContractNotDeployed.tsx +27 -0
- package/templates/react-vite-counter/src/components/CounterCard.tsx +1 -1
- package/templates/react-vite-counter/src/components/WalletButton.tsx +8 -7
- package/templates/react-vite-counter/src/components/WalletModal.tsx +248 -0
- package/templates/react-vite-counter/src/stubs/empty-wallet-dep/index.cjs +3 -0
- package/templates/react-vite-counter/src/stubs/empty-wallet-dep/package.json +6 -0
- package/templates/react-vite-counter/src/stubs/hot-wallet-sdk/index.cjs +7 -0
- package/templates/react-vite-counter/src/stubs/hot-wallet-sdk/package.json +6 -0
- package/templates/react-vite-counter/src/styles.css +261 -0
- package/templates/react-vite-counter/src/wallet-modal-controller.ts +73 -0
- package/templates/react-vite-counter/src/wallet.ts +9 -1
- package/templates/react-vite-counter/vite.config.ts +17 -1
- package/templates/react-vite-counter/src/context/WalletContext.tsx +0 -64
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @caatinga/cli
|
|
2
2
|
|
|
3
|
-
Developer toolkit for Stellar / Soroban dApps — `init`, `build`, `deploy`, `generate`, and `invoke`.
|
|
3
|
+
Developer toolkit for Stellar / Soroban dApps — `init`, `build`, `deploy`, `generate`, `status`, and `invoke`.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -35,11 +35,11 @@ npm install
|
|
|
35
35
|
|
|
36
36
|
npx caatinga build counter
|
|
37
37
|
npx caatinga deploy counter --network testnet --source alice
|
|
38
|
-
npx caatinga
|
|
38
|
+
npx caatinga status --network testnet
|
|
39
39
|
npx caatinga invoke counter.increment --network testnet --source alice
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
`build` only compiles the WASM file. `deploy` writes contract IDs to `caatinga.artifacts.json
|
|
42
|
+
`build` only compiles the WASM file. `deploy` writes contract IDs to `caatinga.artifacts.json` and then generates TypeScript bindings automatically under the path configured in `caatinga.config.ts` (templates default to `contracts/generated/`); pass `--no-generate` to skip. `status` shows what's deployed per network and whether bindings are fresh.
|
|
43
43
|
|
|
44
44
|
## Commands
|
|
45
45
|
|
|
@@ -49,10 +49,11 @@ npx caatinga invoke counter.increment --network testnet --source alice
|
|
|
49
49
|
| `caatinga doctor [--network <network>] [--source <identity>]` | Check local Node, Stellar CLI, Rust, config, artifacts, network, and source identity setup |
|
|
50
50
|
| `caatinga build [contract]` | Compile contract WASM through Stellar CLI (default contract: `counter`) |
|
|
51
51
|
| `caatinga deploy [contract]` | Deploy one contract or the full configured graph; record IDs in artifacts |
|
|
52
|
-
| `caatinga generate [contract]` |
|
|
52
|
+
| `caatinga generate [contract]` | (Re)generate TypeScript bindings; omit the name to generate for all deployed contracts |
|
|
53
|
+
| `caatinga status [--network <name>] [--json]` | Show deployed contracts and binding freshness per network |
|
|
53
54
|
| `caatinga invoke <contract.method>` | Invoke a deployed contract method; extra args forward to Stellar CLI |
|
|
54
55
|
|
|
55
|
-
The supported CLI flow is `init -> build -> deploy
|
|
56
|
+
The supported CLI flow is `init -> build -> deploy (bindings auto-generate) -> invoke`.
|
|
56
57
|
|
|
57
58
|
### `init`
|
|
58
59
|
|
|
@@ -78,12 +79,17 @@ The supported CLI flow is `init -> build -> deploy -> generate -> invoke`.
|
|
|
78
79
|
- `-s, --source <identity>` is required; must be a Stellar CLI identity alias that can sign (for example `alice`)
|
|
79
80
|
- `--force` redeploys even when artifacts already store a contract ID
|
|
80
81
|
- `--no-deps` skips dependency deployment for a single named contract (`--no-deps` requires `[contract]`)
|
|
82
|
+
- `--no-generate` skips the automatic bindings generation after deploy
|
|
81
83
|
|
|
82
84
|
Dependencies listed in `dependsOn` deploy first unless `--no-deps` is set. Deploy args may reference `${contracts.<name>.contractId}` placeholders resolved from artifacts.
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
After a successful deploy, bindings generate automatically for the deployed contracts. A generation failure never fails the deploy — the CLI prints a warning plus the recovery command (`npx caatinga generate --network <network>`).
|
|
87
|
+
|
|
88
|
+
### `generate`, `status`, and `invoke`
|
|
85
89
|
|
|
86
90
|
- `-n, --network <network>` selects the network used to resolve deployed contract IDs
|
|
91
|
+
- `generate` prints binding freshness per contract before regenerating in all-contracts mode
|
|
92
|
+
- `status` prints a per-network table (contract ID, WASM hash, deployed, binding freshness, dependencies); `--json` emits the machine-readable structure
|
|
87
93
|
- `invoke` expects `<contract.method>` (for example `counter.increment`) and forwards `[args...]` to the underlying Stellar invocation
|
|
88
94
|
|
|
89
95
|
`caatinga dev` is reserved, hidden in pre-v1 builds, and not part of the stability promise. Use your frontend dev server (for example Vite) alongside the commands above.
|
package/dist/index.js
CHANGED
|
@@ -161,9 +161,16 @@ async function warnIfDefaultNetworkNeedsDeploy(config) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// src/commands/deploy.command.ts
|
|
164
|
-
import {
|
|
164
|
+
import {
|
|
165
|
+
deployContractGraph,
|
|
166
|
+
generateBindingsGraph,
|
|
167
|
+
toCaatingaError as toCaatingaError2,
|
|
168
|
+
CaatingaError as CaatingaError2,
|
|
169
|
+
CaatingaErrorCode as CaatingaErrorCode2,
|
|
170
|
+
loadConfig as loadConfig3
|
|
171
|
+
} from "@caatinga/core";
|
|
165
172
|
function registerDeployCommand(program2) {
|
|
166
|
-
program2.command("deploy").description("Deploy one or all configured Soroban contracts").argument("[contract]", "Contract name").option("-n, --network <network>", "Configured network name").requiredOption("-s, --source <source>", "Stellar CLI identity alias that can sign (for example alice)").option("--force", "Redeploy contracts even if artifacts already contain contract IDs").option("--no-deps", "Do not deploy missing dependencies for a selected contract").option("--no-stale-check", "Do not warn when WASM may be older than contract sources").option("--verify-deps", "Verify dependency contract IDs exist on-chain before deploy").action((contractName, options) => runCliAction(async () => {
|
|
173
|
+
program2.command("deploy").description("Deploy one or all configured Soroban contracts").argument("[contract]", "Contract name").option("-n, --network <network>", "Configured network name").requiredOption("-s, --source <source>", "Stellar CLI identity alias that can sign (for example alice)").option("--force", "Redeploy contracts even if artifacts already contain contract IDs").option("--no-deps", "Do not deploy missing dependencies for a selected contract").option("--no-stale-check", "Do not warn when WASM may be older than contract sources").option("--verify-deps", "Verify dependency contract IDs exist on-chain before deploy").option("--no-generate", "Skip TypeScript bindings generation after deploy").action((contractName, options) => runCliAction(async () => {
|
|
167
174
|
if (options.deps === false && !contractName) {
|
|
168
175
|
throw new CaatingaError2(
|
|
169
176
|
"`--no-deps` requires a contract name.",
|
|
@@ -197,15 +204,44 @@ function registerDeployCommand(program2) {
|
|
|
197
204
|
logger.info(` Contract ID: ${contract.contractId}`);
|
|
198
205
|
}
|
|
199
206
|
logger.info("Artifacts updated: caatinga.artifacts.json");
|
|
200
|
-
if (result.deployedContracts.length
|
|
207
|
+
if (result.deployedContracts.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (options.generate === false) {
|
|
201
211
|
logger.info("");
|
|
212
|
+
logger.info("Bindings generation skipped (--no-generate).");
|
|
202
213
|
logger.info("Next:");
|
|
203
214
|
for (const contract of result.deployedContracts) {
|
|
204
215
|
logger.info(` npx caatinga generate ${contract.name} --network ${result.network.name}`);
|
|
205
216
|
}
|
|
206
217
|
logger.info(" npm run dev");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const generated = await generateBindingsGraph({
|
|
222
|
+
config,
|
|
223
|
+
contractNames: result.deployedContracts.map((contract) => contract.name),
|
|
224
|
+
networkName: result.network.name
|
|
225
|
+
});
|
|
226
|
+
logger.info("");
|
|
227
|
+
logger.success("Bindings generated");
|
|
228
|
+
for (const binding of generated.results) {
|
|
229
|
+
logger.info(` ${binding.contractName} \u2192 ${binding.importPath}`);
|
|
230
|
+
}
|
|
231
|
+
logger.info("");
|
|
232
|
+
logger.info("Next:");
|
|
233
|
+
logger.info(" npm run dev");
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const caatingaError = toCaatingaError2(error);
|
|
207
236
|
logger.info("");
|
|
208
|
-
logger.
|
|
237
|
+
logger.warn("Deploy succeeded, but bindings generation failed.");
|
|
238
|
+
logger.warn(` ${caatingaError.message} (${caatingaError.code})`);
|
|
239
|
+
if (caatingaError.hint) {
|
|
240
|
+
logger.warn(` Hint: ${caatingaError.hint}`);
|
|
241
|
+
}
|
|
242
|
+
logger.info("");
|
|
243
|
+
logger.info("Recover with:");
|
|
244
|
+
logger.info(` npx caatinga generate --network ${result.network.name}`);
|
|
209
245
|
}
|
|
210
246
|
}));
|
|
211
247
|
}
|
|
@@ -398,6 +434,33 @@ function printFixes(diagnostics) {
|
|
|
398
434
|
}
|
|
399
435
|
}
|
|
400
436
|
|
|
437
|
+
// src/commands/doctor-bindings.ts
|
|
438
|
+
import {
|
|
439
|
+
evaluateBindingsFreshness,
|
|
440
|
+
loadConfig as loadConfig5,
|
|
441
|
+
readArtifacts as readArtifacts3,
|
|
442
|
+
resolveNetwork as resolveNetwork3
|
|
443
|
+
} from "@caatinga/core";
|
|
444
|
+
async function evaluateBindingCoverage(options) {
|
|
445
|
+
const cwd = options.cwd;
|
|
446
|
+
const config = await loadConfig5({ cwd });
|
|
447
|
+
const network = resolveNetwork3(config, options.networkName);
|
|
448
|
+
const artifacts = await readArtifacts3(cwd);
|
|
449
|
+
const freshness = await evaluateBindingsFreshness({
|
|
450
|
+
config,
|
|
451
|
+
artifacts,
|
|
452
|
+
networkName: network.name,
|
|
453
|
+
cwd
|
|
454
|
+
});
|
|
455
|
+
const lines = freshness.map((entry) => ({
|
|
456
|
+
name: entry.contractName,
|
|
457
|
+
status: entry.status,
|
|
458
|
+
reason: entry.reason,
|
|
459
|
+
...entry.status === "fresh" ? {} : { fix: `Run: caatinga generate ${entry.contractName} --network ${network.name}` }
|
|
460
|
+
}));
|
|
461
|
+
return { lines, allFresh: lines.every((line) => line.status === "fresh") };
|
|
462
|
+
}
|
|
463
|
+
|
|
401
464
|
// src/commands/doctor.command.ts
|
|
402
465
|
function printDeployCoverageLine(line) {
|
|
403
466
|
if (line.ok) {
|
|
@@ -424,6 +487,25 @@ async function reportDeployCoverage(networkName) {
|
|
|
424
487
|
}
|
|
425
488
|
return true;
|
|
426
489
|
}
|
|
490
|
+
function printBindingCoverageLine(line) {
|
|
491
|
+
if (line.status === "fresh") {
|
|
492
|
+
logger.info(`\u2713 ${line.name} \u2014 bindings fresh`);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
logger.info(`\u2717 ${line.name} \u2014 bindings ${line.status}${line.reason ? ` (${line.reason})` : ""}`);
|
|
496
|
+
if (line.fix) logger.info(` ${line.fix}`);
|
|
497
|
+
}
|
|
498
|
+
async function reportBindingCoverage(networkName) {
|
|
499
|
+
const coverage = await evaluateBindingCoverage({ networkName });
|
|
500
|
+
if (coverage.lines.length === 0) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
logger.info("");
|
|
504
|
+
logger.info(`Bindings (${networkName}):`);
|
|
505
|
+
for (const line of coverage.lines) {
|
|
506
|
+
printBindingCoverageLine(line);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
427
509
|
function registerDoctorCommand(program2) {
|
|
428
510
|
program2.command("doctor").description("Check local Caatinga, Stellar CLI, Rust, config, and source identity setup").option("-n, --network <network>", "Configured network name to validate").option("-s, --source <source>", "Stellar CLI identity alias to validate").action((options) => runCliAction(async () => {
|
|
429
511
|
logger.info("Caatinga Doctor");
|
|
@@ -441,6 +523,7 @@ function registerDoctorCommand(program2) {
|
|
|
441
523
|
ready = false;
|
|
442
524
|
throw error;
|
|
443
525
|
}
|
|
526
|
+
await reportBindingCoverage(options.network);
|
|
444
527
|
}
|
|
445
528
|
logger.info("");
|
|
446
529
|
logger.info(`Status: ${ready ? "ready" : "blocked"}`);
|
|
@@ -451,11 +534,41 @@ function registerDoctorCommand(program2) {
|
|
|
451
534
|
}
|
|
452
535
|
|
|
453
536
|
// src/commands/generate.command.ts
|
|
454
|
-
import {
|
|
537
|
+
import {
|
|
538
|
+
evaluateBindingsFreshness as evaluateBindingsFreshness2,
|
|
539
|
+
generateBindingsGraph as generateBindingsGraph2,
|
|
540
|
+
loadConfig as loadConfig6,
|
|
541
|
+
readArtifacts as readArtifacts4,
|
|
542
|
+
resolveNetwork as resolveNetwork4
|
|
543
|
+
} from "@caatinga/core";
|
|
544
|
+
async function printFreshnessPreState(config, networkName) {
|
|
545
|
+
try {
|
|
546
|
+
const network = resolveNetwork4(config, networkName);
|
|
547
|
+
const artifacts = await readArtifacts4();
|
|
548
|
+
const freshness = await evaluateBindingsFreshness2({
|
|
549
|
+
config,
|
|
550
|
+
artifacts,
|
|
551
|
+
networkName: network.name
|
|
552
|
+
});
|
|
553
|
+
if (freshness.length === 0) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
logger.info("Current bindings:");
|
|
557
|
+
for (const entry of freshness) {
|
|
558
|
+
const reason = entry.reason ? ` \u2014 ${entry.reason}` : "";
|
|
559
|
+
logger.info(` [${entry.status}] ${entry.contractName}${reason}`);
|
|
560
|
+
}
|
|
561
|
+
logger.info("");
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
455
565
|
function registerGenerateCommand(program2) {
|
|
456
566
|
program2.command("generate").description("Generate TypeScript bindings for deployed contracts").argument("[contract]", "Contract name (defaults to all deployed contracts)").option("-n, --network <network>", "Configured network name").action((contractName, options) => runCliAction(async () => {
|
|
457
|
-
const config = await
|
|
458
|
-
|
|
567
|
+
const config = await loadConfig6();
|
|
568
|
+
if (!contractName) {
|
|
569
|
+
await printFreshnessPreState(config, options.network);
|
|
570
|
+
}
|
|
571
|
+
const { network, results } = await generateBindingsGraph2({
|
|
459
572
|
config,
|
|
460
573
|
contractName,
|
|
461
574
|
networkName: options.network
|
|
@@ -562,26 +675,25 @@ function registerInitCommand(program2) {
|
|
|
562
675
|
logger.info(
|
|
563
676
|
` npx caatinga deploy ${defaultContract} --network testnet --source <identity>`
|
|
564
677
|
);
|
|
565
|
-
logger.info(` npx caatinga generate ${defaultContract} --network testnet`);
|
|
566
678
|
} else {
|
|
567
679
|
logger.info(" npx caatinga build");
|
|
568
680
|
logger.info(" npx caatinga deploy --network testnet --source <identity>");
|
|
569
|
-
logger.info(" npx caatinga generate --network testnet");
|
|
570
681
|
}
|
|
571
682
|
logger.info(" npm run dev");
|
|
572
683
|
logger.info("");
|
|
573
684
|
logger.info(
|
|
574
|
-
"Note: deploy
|
|
685
|
+
"Note: deploy generates TypeScript bindings automatically (--no-generate to skip) \u2014"
|
|
575
686
|
);
|
|
576
687
|
logger.info("the dApp reads the contract ID from caatinga.artifacts.json.");
|
|
688
|
+
logger.info("If generation fails, recover with: npx caatinga generate --network testnet");
|
|
577
689
|
}));
|
|
578
690
|
}
|
|
579
691
|
|
|
580
692
|
// src/commands/invoke.command.ts
|
|
581
|
-
import { invokeContract, loadConfig as
|
|
693
|
+
import { invokeContract, loadConfig as loadConfig7 } from "@caatinga/core";
|
|
582
694
|
function registerInvokeCommand(program2) {
|
|
583
695
|
program2.command("invoke").description("Invoke a deployed contract function").argument("<target>", "Invoke target in contract.method format").argument("[args...]", "Arguments forwarded to Stellar CLI after the method name").option("-n, --network <network>", "Configured network name").requiredOption("-s, --source <source>", "Stellar CLI identity alias that can sign (for example alice)").allowUnknownOption(true).allowExcessArguments(true).action((target, args, options) => runCliAction(async () => {
|
|
584
|
-
const config = await
|
|
696
|
+
const config = await loadConfig7();
|
|
585
697
|
const result = await invokeContract({
|
|
586
698
|
config,
|
|
587
699
|
target,
|
|
@@ -601,8 +713,77 @@ function registerInvokeCommand(program2) {
|
|
|
601
713
|
}));
|
|
602
714
|
}
|
|
603
715
|
|
|
716
|
+
// src/commands/status.command.ts
|
|
717
|
+
import { collectProjectStatus, loadConfig as loadConfig8 } from "@caatinga/core";
|
|
718
|
+
|
|
719
|
+
// src/utils/table.ts
|
|
720
|
+
function renderTable(headers, rows) {
|
|
721
|
+
const widths = headers.map(
|
|
722
|
+
(header, column) => Math.max(header.length, ...rows.map((row) => (row[column] ?? "").length))
|
|
723
|
+
);
|
|
724
|
+
const renderRow = (cells) => cells.map((cell, column) => (cell ?? "").padEnd(widths[column])).join(" ").trimEnd();
|
|
725
|
+
return [
|
|
726
|
+
renderRow(headers),
|
|
727
|
+
widths.map((width) => "\u2500".repeat(width)).join(" "),
|
|
728
|
+
...rows.map((row) => renderRow(row))
|
|
729
|
+
];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/commands/status.command.ts
|
|
733
|
+
function shortId(value) {
|
|
734
|
+
if (!value) return "\u2014";
|
|
735
|
+
return value.length > 12 ? `${value.slice(0, 5)}\u2026${value.slice(-4)}` : value;
|
|
736
|
+
}
|
|
737
|
+
function shortHash(value) {
|
|
738
|
+
if (!value) return "\u2014";
|
|
739
|
+
return value.slice(0, 8);
|
|
740
|
+
}
|
|
741
|
+
function toRow(entry) {
|
|
742
|
+
return [
|
|
743
|
+
entry.name,
|
|
744
|
+
shortId(entry.contractId),
|
|
745
|
+
shortHash(entry.wasmHash),
|
|
746
|
+
entry.deployed ? "\u2713" : "\u2717",
|
|
747
|
+
entry.bindings.status,
|
|
748
|
+
entry.dependencies.length > 0 ? entry.dependencies.join(", ") : "\u2014"
|
|
749
|
+
];
|
|
750
|
+
}
|
|
751
|
+
function registerStatusCommand(program2) {
|
|
752
|
+
program2.command("status").description("Show deployed contracts and binding freshness per network").option("-n, --network <network>", "Configured network name").option("--json", "Print machine-readable JSON instead of the table").action((options) => runCliAction(async () => {
|
|
753
|
+
const config = await loadConfig8();
|
|
754
|
+
const status = await collectProjectStatus({
|
|
755
|
+
config,
|
|
756
|
+
networkName: options.network
|
|
757
|
+
});
|
|
758
|
+
if (options.json) {
|
|
759
|
+
console.log(JSON.stringify(status, null, 2));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
logger.success(`Project: ${status.project}`);
|
|
763
|
+
for (const network of status.networks) {
|
|
764
|
+
logger.info("");
|
|
765
|
+
logger.info(`Network: ${network.network}`);
|
|
766
|
+
const lines = renderTable(
|
|
767
|
+
["CONTRACT", "CONTRACT ID", "WASM HASH", "DEPLOYED", "BINDINGS", "DEPS"],
|
|
768
|
+
network.contracts.map(toRow)
|
|
769
|
+
);
|
|
770
|
+
for (const line of lines) {
|
|
771
|
+
logger.info(line);
|
|
772
|
+
}
|
|
773
|
+
const needsAttention = network.contracts.filter(
|
|
774
|
+
(entry) => entry.deployed && entry.bindings.status !== "fresh"
|
|
775
|
+
);
|
|
776
|
+
for (const entry of needsAttention) {
|
|
777
|
+
logger.warn(
|
|
778
|
+
`Bindings ${entry.bindings.status} for ${entry.name}${entry.bindings.reason ? ` (${entry.bindings.reason})` : ""} \u2014 run: npx caatinga generate ${entry.name} --network ${network.network}`
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
|
|
604
785
|
// src/version.ts
|
|
605
|
-
var CAATINGA_CLI_VERSION = "2.
|
|
786
|
+
var CAATINGA_CLI_VERSION = "2.2.0";
|
|
606
787
|
|
|
607
788
|
// src/program.ts
|
|
608
789
|
function createProgram() {
|
|
@@ -615,6 +796,7 @@ function createProgram() {
|
|
|
615
796
|
registerDeployCommand(program2);
|
|
616
797
|
registerGenerateCommand(program2);
|
|
617
798
|
registerInvokeCommand(program2);
|
|
799
|
+
registerStatusCommand(program2);
|
|
618
800
|
return program2;
|
|
619
801
|
}
|
|
620
802
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caatinga/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Caatinga CLI for building dApps on Stellar/Soroban",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"stellar",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"LICENSE"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@caatinga/core": "^2.
|
|
46
|
+
"@caatinga/core": "^2.2.0",
|
|
47
47
|
"chalk": "^5.4.1",
|
|
48
48
|
"commander": "^12.1.0"
|
|
49
49
|
},
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"vitest": "^2.1.8"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
|
-
"build": "tsup src/index.ts --format esm --dts --clean && rm -rf ./templates &&
|
|
57
|
+
"build": "tsup src/index.ts --format esm --dts --clean && rm -rf ./templates && mkdir ./templates && (cd ../templates && tar --exclude=node_modules --exclude=dist -cf - .) | (cd ./templates && tar -xf -)",
|
|
58
58
|
"predev": "pnpm --filter @caatinga/core build",
|
|
59
59
|
"dev": "tsx src/index.ts",
|
|
60
60
|
"test": "vitest run --pool=threads",
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
"caatinga:generate": "caatinga generate token && caatinga generate marketplace"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@caatinga/client": "^2.
|
|
16
|
-
"@caatinga/core": "^2.
|
|
15
|
+
"@caatinga/client": "^2.2.0",
|
|
16
|
+
"@caatinga/core": "^2.2.0",
|
|
17
17
|
"@vitejs/plugin-react": "^4.3.4",
|
|
18
18
|
"react": "^18.3.1",
|
|
19
19
|
"react-dom": "^18.3.1",
|
|
20
20
|
"vite": "^6.0.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@caatinga/cli": "^2.
|
|
23
|
+
"@caatinga/cli": "^2.2.0",
|
|
24
24
|
"@types/react": "^18.3.18",
|
|
25
25
|
"@types/react-dom": "^18.3.5",
|
|
26
26
|
"typescript": "^5.7.2"
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "tsc && vite build",
|
|
9
9
|
"preview": "vite preview",
|
|
10
|
-
"caatinga:build": "caatinga build counter",
|
|
11
|
-
"caatinga:deploy": "caatinga deploy counter",
|
|
12
|
-
"caatinga:generate": "caatinga generate counter"
|
|
10
|
+
"caatinga:build": "npx caatinga build counter",
|
|
11
|
+
"caatinga:deploy": "npx caatinga deploy counter --network testnet --source ${CAATINGA_SOURCE:-alice}",
|
|
12
|
+
"caatinga:generate": "npx caatinga generate counter --network testnet"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@caatinga/client": "^2.
|
|
16
|
-
"@caatinga/core": "^2.
|
|
17
|
-
"@creit.tech/stellar-wallets-kit": "^
|
|
15
|
+
"@caatinga/client": "^2.2.0",
|
|
16
|
+
"@caatinga/core": "^2.2.0",
|
|
17
|
+
"@creit.tech/stellar-wallets-kit": "^2.3.0",
|
|
18
18
|
"@stellar/stellar-sdk": "^14.5.0",
|
|
19
19
|
"buffer": "^6.0.3",
|
|
20
20
|
"@vitejs/plugin-react": "^4.3.4",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"vite": "^6.0.6"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@caatinga/cli": "^2.
|
|
26
|
+
"@caatinga/cli": "^2.2.0",
|
|
27
27
|
"@types/react": "^18.3.18",
|
|
28
28
|
"@types/react-dom": "^18.3.5",
|
|
29
29
|
"typescript": "^5.7.2"
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
# pnpm
|
|
1
|
+
# pnpm-only file (npm / yarn / bun ignore it). pnpm 10.26+/11.x block lifecycle
|
|
2
|
+
# scripts by default; vite depends on esbuild's, so allow it. All dependency
|
|
3
|
+
# stubbing now happens at the build layer (see vite.config.ts), so no pnpm-only
|
|
4
|
+
# `overrides` are needed and this template installs identically on every PM.
|
|
2
5
|
allowBuilds:
|
|
3
6
|
esbuild: true
|
|
4
|
-
|
|
5
|
-
# stellar-wallets-kit@0.0.7 is unpublished from npm and is therefore pinned
|
|
6
|
-
# to a github: URL in package.json. Its own dependencies also reference
|
|
7
|
-
# @creit.tech/xbull-wallet-connect via a github: URL, and pnpm 10.26+ (and
|
|
8
|
-
# 11.x) defaults blockExoticSubdeps to true, which would refuse to install
|
|
9
|
-
# that exotic subdep. Allow it explicitly. This is a targeted opt-out for
|
|
10
|
-
# this one transitive dep; direct dependencies in package.json still must
|
|
11
|
-
# come from a trusted source.
|
|
12
|
-
blockExoticSubdeps: false
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { WalletProvider, useWallet } from "@caatinga/client/react";
|
|
2
|
+
import type { CaatingaArtifacts } from "@caatinga/core/browser";
|
|
3
|
+
import artifactsJson from "../caatinga.artifacts.json";
|
|
4
|
+
import { ContractNotDeployed } from "./components/ContractNotDeployed";
|
|
1
5
|
import { CounterCard } from "./components/CounterCard";
|
|
2
6
|
import { WalletButton } from "./components/WalletButton";
|
|
3
|
-
import {
|
|
7
|
+
import { WalletModal } from "./components/WalletModal";
|
|
8
|
+
import { stellarWalletAdapter } from "./wallet.js";
|
|
9
|
+
|
|
10
|
+
const artifacts = artifactsJson as CaatingaArtifacts;
|
|
11
|
+
const counterContractId = artifacts.networks?.testnet?.contracts?.counter?.contractId;
|
|
12
|
+
const isDeployed = Boolean(counterContractId);
|
|
4
13
|
|
|
5
14
|
function AppBody() {
|
|
6
15
|
const { publicKey } = useWallet();
|
|
@@ -15,7 +24,9 @@ function AppBody() {
|
|
|
15
24
|
<WalletButton />
|
|
16
25
|
</header>
|
|
17
26
|
|
|
18
|
-
{
|
|
27
|
+
{!isDeployed ? (
|
|
28
|
+
<ContractNotDeployed />
|
|
29
|
+
) : publicKey ? (
|
|
19
30
|
<CounterCard />
|
|
20
31
|
) : (
|
|
21
32
|
<section className="counter-panel" aria-labelledby="connect-title">
|
|
@@ -35,8 +46,11 @@ function AppBody() {
|
|
|
35
46
|
|
|
36
47
|
export default function App() {
|
|
37
48
|
return (
|
|
38
|
-
|
|
49
|
+
// persist keeps the session across reloads; the provider silently
|
|
50
|
+
// reconnects on mount (autoConnect defaults to true when persisting).
|
|
51
|
+
<WalletProvider adapter={stellarWalletAdapter} options={{ persist: true }}>
|
|
39
52
|
<AppBody />
|
|
53
|
+
<WalletModal />
|
|
40
54
|
</WalletProvider>
|
|
41
55
|
);
|
|
42
56
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function ContractNotDeployed() {
|
|
2
|
+
return (
|
|
3
|
+
<section className="counter-panel" aria-labelledby="not-deployed-title">
|
|
4
|
+
<div className="counter-panel__header">
|
|
5
|
+
<div>
|
|
6
|
+
<p className="eyebrow">Get started</p>
|
|
7
|
+
<h2 id="not-deployed-title">Contract not deployed</h2>
|
|
8
|
+
</div>
|
|
9
|
+
<span className="network-pill">testnet</span>
|
|
10
|
+
</div>
|
|
11
|
+
<p>
|
|
12
|
+
The counter contract has no on-chain ID yet, so the frontend can't read or update it.
|
|
13
|
+
Build and deploy first — the dApp reads the contract ID from{" "}
|
|
14
|
+
<code>caatinga.artifacts.json</code>. Deploy also generates TypeScript bindings
|
|
15
|
+
automatically.
|
|
16
|
+
</p>
|
|
17
|
+
<pre className="counter-error" role="note">
|
|
18
|
+
{`npx caatinga build counter
|
|
19
|
+
npx caatinga deploy counter --network testnet --source <identity>
|
|
20
|
+
npm run dev
|
|
21
|
+
|
|
22
|
+
# If bindings generation failed after deploy:
|
|
23
|
+
npx caatinga generate counter --network testnet`}
|
|
24
|
+
</pre>
|
|
25
|
+
</section>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { caatingaClient } from "../caatinga.js";
|
|
3
3
|
import { formatCaatingaError } from "@caatinga/core/browser";
|
|
4
|
-
import { useWallet } from "
|
|
4
|
+
import { useWallet } from "@caatinga/client/react";
|
|
5
5
|
import { LoadingModal } from "./LoadingModal.js";
|
|
6
6
|
|
|
7
7
|
export function CounterCard() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useWallet } from "
|
|
1
|
+
import { useWallet } from "@caatinga/client/react";
|
|
2
|
+
import { WALLET_SELECTION_CLOSED_ERROR } from "../wallet-modal-controller.js";
|
|
2
3
|
|
|
3
4
|
function shortenAddress(address: string): string {
|
|
4
5
|
if (address.length <= 12) {
|
|
@@ -9,23 +10,23 @@ function shortenAddress(address: string): string {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function WalletButton() {
|
|
12
|
-
const { publicKey,
|
|
13
|
+
const { publicKey, connecting, error, connect, disconnect } = useWallet();
|
|
13
14
|
|
|
14
15
|
return (
|
|
15
16
|
<div className="wallet-shell">
|
|
16
17
|
<button
|
|
17
18
|
className="wallet-button"
|
|
18
19
|
type="button"
|
|
19
|
-
onClick={publicKey ? () => void disconnect() : () => void connect()}
|
|
20
|
-
disabled={
|
|
20
|
+
onClick={publicKey ? () => void disconnect() : () => void connect().catch(() => {})}
|
|
21
|
+
disabled={connecting}
|
|
21
22
|
aria-live="polite"
|
|
22
23
|
>
|
|
23
24
|
<span className={publicKey ? "status-dot status-dot--on" : "status-dot"} />
|
|
24
|
-
{
|
|
25
|
+
{connecting ? "Connecting..." : publicKey ? shortenAddress(publicKey) : "Connect"}
|
|
25
26
|
</button>
|
|
26
|
-
{error ? (
|
|
27
|
+
{error && error.name !== WALLET_SELECTION_CLOSED_ERROR ? (
|
|
27
28
|
<p className="wallet-error" role="alert">
|
|
28
|
-
{error}
|
|
29
|
+
{error.message}
|
|
29
30
|
</p>
|
|
30
31
|
) : null}
|
|
31
32
|
</div>
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { formatCaatingaError } from "@caatinga/core/browser";
|
|
3
|
+
import type { ISupportedWallet } from "@creit.tech/stellar-wallets-kit/types";
|
|
4
|
+
import { stellarWalletAdapter } from "../wallet.js";
|
|
5
|
+
import {
|
|
6
|
+
cancelWalletSelection,
|
|
7
|
+
getWalletModalState,
|
|
8
|
+
resolveWalletSelection,
|
|
9
|
+
subscribeWalletModal
|
|
10
|
+
} from "../wallet-modal-controller.js";
|
|
11
|
+
|
|
12
|
+
const LEARN_MORE_URL = "https://developers.stellar.org/docs/build/apps/wallet/overview";
|
|
13
|
+
|
|
14
|
+
// SWK wallet modules reject with plain `{ code, message }` objects, not Errors.
|
|
15
|
+
function describeWalletError(caught: unknown): string {
|
|
16
|
+
if (caught && typeof caught === "object" && "message" in caught) {
|
|
17
|
+
const message = (caught as { message?: unknown }).message;
|
|
18
|
+
if (typeof message === "string" && message) {
|
|
19
|
+
return message;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return formatCaatingaError(caught);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function walletBadge(wallet: ISupportedWallet): string | null {
|
|
26
|
+
if (!wallet.isAvailable) {
|
|
27
|
+
return "Install ↗";
|
|
28
|
+
}
|
|
29
|
+
// Bridge wallets (WalletConnect) hand off to a QR code instead of an extension.
|
|
30
|
+
return wallet.type === "BRIDGE_WALLET" ? "QR code" : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface WalletOptionProps {
|
|
34
|
+
wallet: ISupportedWallet;
|
|
35
|
+
connectingId: string | null;
|
|
36
|
+
onSelect(wallet: ISupportedWallet): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function WalletOption({ wallet, connectingId, onSelect }: WalletOptionProps) {
|
|
40
|
+
const connecting = connectingId === wallet.id;
|
|
41
|
+
const badge = walletBadge(wallet);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<li>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
className={connecting ? "wallet-option wallet-option--connecting" : "wallet-option"}
|
|
48
|
+
onClick={() => onSelect(wallet)}
|
|
49
|
+
disabled={connectingId !== null && !connecting}
|
|
50
|
+
>
|
|
51
|
+
<img className="wallet-option__icon" src={wallet.icon} alt="" />
|
|
52
|
+
<span className="wallet-option__name">
|
|
53
|
+
{connecting ? `Connecting to ${wallet.name}…` : wallet.name}
|
|
54
|
+
</span>
|
|
55
|
+
{connecting ? (
|
|
56
|
+
<span className="wallet-option__spinner" aria-hidden="true" />
|
|
57
|
+
) : badge ? (
|
|
58
|
+
<span className="wallet-option__badge">{badge}</span>
|
|
59
|
+
) : (
|
|
60
|
+
<span className="status-dot status-dot--on" aria-hidden="true" />
|
|
61
|
+
)}
|
|
62
|
+
</button>
|
|
63
|
+
</li>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function WalletModal() {
|
|
68
|
+
const { open } = useSyncExternalStore(
|
|
69
|
+
subscribeWalletModal,
|
|
70
|
+
getWalletModalState,
|
|
71
|
+
getWalletModalState
|
|
72
|
+
);
|
|
73
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
74
|
+
const [wallets, setWallets] = useState<ISupportedWallet[] | null>(null);
|
|
75
|
+
const [connectingId, setConnectingId] = useState<string | null>(null);
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!open) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setWallets(null);
|
|
84
|
+
setConnectingId(null);
|
|
85
|
+
setError(null);
|
|
86
|
+
|
|
87
|
+
// Availability probes can take a moment; ignore the result if the modal
|
|
88
|
+
// closed (or reopened) before they resolve.
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
stellarWalletAdapter
|
|
91
|
+
.getSupportedWallets()
|
|
92
|
+
.then((list) => {
|
|
93
|
+
if (!cancelled) {
|
|
94
|
+
setWallets(list);
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.catch((caught: unknown) => {
|
|
98
|
+
if (!cancelled) {
|
|
99
|
+
setWallets([]);
|
|
100
|
+
setError(describeWalletError(caught));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
panelRef.current?.focus();
|
|
105
|
+
|
|
106
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
107
|
+
if (event.key === "Escape") {
|
|
108
|
+
cancelWalletSelection();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
window.addEventListener("keydown", onKeyDown);
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
116
|
+
};
|
|
117
|
+
}, [open]);
|
|
118
|
+
|
|
119
|
+
if (!open) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function selectWallet(wallet: ISupportedWallet) {
|
|
124
|
+
if (connectingId) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!wallet.isAvailable) {
|
|
129
|
+
window.open(wallet.url, "_blank", "noopener");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setError(null);
|
|
134
|
+
setConnectingId(wallet.id);
|
|
135
|
+
try {
|
|
136
|
+
stellarWalletAdapter.setWallet(wallet.id);
|
|
137
|
+
const address = await stellarWalletAdapter.getPublicKey();
|
|
138
|
+
resolveWalletSelection(address);
|
|
139
|
+
} catch (caught) {
|
|
140
|
+
setError(describeWalletError(caught));
|
|
141
|
+
} finally {
|
|
142
|
+
setConnectingId(null);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const detected = wallets?.filter((wallet) => wallet.isAvailable) ?? [];
|
|
147
|
+
const others = wallets?.filter((wallet) => !wallet.isAvailable) ?? [];
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className="wallet-modal"
|
|
152
|
+
role="presentation"
|
|
153
|
+
onClick={(event) => {
|
|
154
|
+
if (event.target === event.currentTarget) {
|
|
155
|
+
cancelWalletSelection();
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<div
|
|
160
|
+
ref={panelRef}
|
|
161
|
+
className="wallet-modal__panel"
|
|
162
|
+
role="dialog"
|
|
163
|
+
aria-modal="true"
|
|
164
|
+
aria-labelledby="wallet-modal-title"
|
|
165
|
+
tabIndex={-1}
|
|
166
|
+
>
|
|
167
|
+
<header className="wallet-modal__header">
|
|
168
|
+
<h2 id="wallet-modal-title">Connect a wallet</h2>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className="wallet-modal__close"
|
|
172
|
+
onClick={() => cancelWalletSelection()}
|
|
173
|
+
aria-label="Close"
|
|
174
|
+
>
|
|
175
|
+
✕
|
|
176
|
+
</button>
|
|
177
|
+
</header>
|
|
178
|
+
|
|
179
|
+
{error ? (
|
|
180
|
+
<p className="wallet-modal__error" role="alert">
|
|
181
|
+
{error}
|
|
182
|
+
</p>
|
|
183
|
+
) : null}
|
|
184
|
+
|
|
185
|
+
<div className="wallet-modal__body">
|
|
186
|
+
{wallets === null ? (
|
|
187
|
+
<ul className="wallet-modal__list" aria-label="Loading wallets">
|
|
188
|
+
{[0, 1, 2].map((row) => (
|
|
189
|
+
<li key={row} className="wallet-option wallet-option--skeleton" aria-hidden="true">
|
|
190
|
+
<span className="wallet-skeleton__icon" />
|
|
191
|
+
<span className="wallet-skeleton__line" />
|
|
192
|
+
</li>
|
|
193
|
+
))}
|
|
194
|
+
</ul>
|
|
195
|
+
) : (
|
|
196
|
+
<>
|
|
197
|
+
{detected.length > 0 ? (
|
|
198
|
+
<>
|
|
199
|
+
<p className="eyebrow wallet-modal__section">Detected</p>
|
|
200
|
+
<ul className="wallet-modal__list">
|
|
201
|
+
{detected.map((wallet) => (
|
|
202
|
+
<WalletOption
|
|
203
|
+
key={wallet.id}
|
|
204
|
+
wallet={wallet}
|
|
205
|
+
connectingId={connectingId}
|
|
206
|
+
onSelect={(selected) => void selectWallet(selected)}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</ul>
|
|
210
|
+
</>
|
|
211
|
+
) : null}
|
|
212
|
+
|
|
213
|
+
{others.length > 0 ? (
|
|
214
|
+
<>
|
|
215
|
+
<p className="eyebrow wallet-modal__section">More wallets</p>
|
|
216
|
+
<ul className="wallet-modal__list">
|
|
217
|
+
{others.map((wallet) => (
|
|
218
|
+
<WalletOption
|
|
219
|
+
key={wallet.id}
|
|
220
|
+
wallet={wallet}
|
|
221
|
+
connectingId={connectingId}
|
|
222
|
+
onSelect={(selected) => void selectWallet(selected)}
|
|
223
|
+
/>
|
|
224
|
+
))}
|
|
225
|
+
</ul>
|
|
226
|
+
</>
|
|
227
|
+
) : null}
|
|
228
|
+
|
|
229
|
+
{detected.length === 0 && others.length === 0 && !error ? (
|
|
230
|
+
<p className="wallet-modal__empty">
|
|
231
|
+
No wallets available. Install a Stellar wallet to continue.
|
|
232
|
+
</p>
|
|
233
|
+
) : null}
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<footer className="wallet-modal__footer">
|
|
240
|
+
<span>New to Stellar wallets?</span>
|
|
241
|
+
<a href={LEARN_MORE_URL} target="_blank" rel="noreferrer">
|
|
242
|
+
Learn more ↗
|
|
243
|
+
</a>
|
|
244
|
+
</footer>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -239,6 +239,232 @@ button:hover {
|
|
|
239
239
|
background: #f4f2ec;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
.wallet-modal {
|
|
243
|
+
position: fixed;
|
|
244
|
+
inset: 0;
|
|
245
|
+
z-index: 20;
|
|
246
|
+
display: flex;
|
|
247
|
+
align-items: center;
|
|
248
|
+
justify-content: center;
|
|
249
|
+
padding: 16px;
|
|
250
|
+
background: rgba(32, 35, 42, 0.45);
|
|
251
|
+
backdrop-filter: blur(3px);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.wallet-modal__panel {
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
width: min(400px, 100%);
|
|
258
|
+
max-height: min(600px, calc(100vh - 32px));
|
|
259
|
+
border: 1px solid #d9d5ca;
|
|
260
|
+
border-radius: 12px;
|
|
261
|
+
background: #fffdf7;
|
|
262
|
+
box-shadow: 0 18px 60px rgba(32, 35, 42, 0.22);
|
|
263
|
+
outline: none;
|
|
264
|
+
animation: wallet-modal-pop 0.18s ease;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@keyframes wallet-modal-pop {
|
|
268
|
+
from {
|
|
269
|
+
opacity: 0;
|
|
270
|
+
transform: translateY(8px) scale(0.98);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.wallet-modal__header {
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
justify-content: space-between;
|
|
278
|
+
gap: 16px;
|
|
279
|
+
padding: 20px 20px 8px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.wallet-modal__header h2 {
|
|
283
|
+
margin: 0;
|
|
284
|
+
color: #16181d;
|
|
285
|
+
font-size: 1.2rem;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.wallet-modal__close {
|
|
289
|
+
width: 32px;
|
|
290
|
+
min-height: 32px;
|
|
291
|
+
padding: 0;
|
|
292
|
+
border-radius: 8px;
|
|
293
|
+
background: transparent;
|
|
294
|
+
color: #697076;
|
|
295
|
+
font-size: 0.95rem;
|
|
296
|
+
font-weight: 400;
|
|
297
|
+
line-height: 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.wallet-modal__close:hover {
|
|
301
|
+
background: #f4f2ec;
|
|
302
|
+
color: #20232a;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.wallet-modal__body {
|
|
306
|
+
overflow-y: auto;
|
|
307
|
+
padding: 0 12px 16px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.wallet-modal__section {
|
|
311
|
+
margin: 14px 10px 6px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.wallet-modal__list {
|
|
315
|
+
display: grid;
|
|
316
|
+
gap: 4px;
|
|
317
|
+
margin: 0;
|
|
318
|
+
padding: 0;
|
|
319
|
+
list-style: none;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.wallet-option {
|
|
323
|
+
display: flex;
|
|
324
|
+
align-items: center;
|
|
325
|
+
gap: 12px;
|
|
326
|
+
width: 100%;
|
|
327
|
+
min-height: 56px;
|
|
328
|
+
padding: 8px 12px;
|
|
329
|
+
border: 1px solid transparent;
|
|
330
|
+
border-radius: 10px;
|
|
331
|
+
background: transparent;
|
|
332
|
+
color: #20232a;
|
|
333
|
+
font-weight: 700;
|
|
334
|
+
text-align: left;
|
|
335
|
+
transition:
|
|
336
|
+
background 0.15s ease,
|
|
337
|
+
border-color 0.15s ease;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.wallet-option:hover {
|
|
341
|
+
background: #f4f2ec;
|
|
342
|
+
border-color: #d9d5ca;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.wallet-option:disabled {
|
|
346
|
+
background: transparent;
|
|
347
|
+
border-color: transparent;
|
|
348
|
+
opacity: 0.5;
|
|
349
|
+
cursor: default;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.wallet-option--connecting,
|
|
353
|
+
.wallet-option--connecting:hover {
|
|
354
|
+
background: #f4f2ec;
|
|
355
|
+
border-color: #d9d5ca;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.wallet-option__icon {
|
|
359
|
+
width: 36px;
|
|
360
|
+
height: 36px;
|
|
361
|
+
border: 1px solid #eee9dd;
|
|
362
|
+
border-radius: 8px;
|
|
363
|
+
background: #ffffff;
|
|
364
|
+
object-fit: contain;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.wallet-option__name {
|
|
368
|
+
flex: 1;
|
|
369
|
+
min-width: 0;
|
|
370
|
+
overflow: hidden;
|
|
371
|
+
text-overflow: ellipsis;
|
|
372
|
+
white-space: nowrap;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.wallet-option__badge {
|
|
376
|
+
border: 1px solid #d9d5ca;
|
|
377
|
+
border-radius: 999px;
|
|
378
|
+
padding: 3px 10px;
|
|
379
|
+
background: #ffffff;
|
|
380
|
+
color: #45515a;
|
|
381
|
+
font-size: 0.7rem;
|
|
382
|
+
font-weight: 800;
|
|
383
|
+
letter-spacing: 0.04em;
|
|
384
|
+
text-transform: uppercase;
|
|
385
|
+
white-space: nowrap;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.wallet-option__spinner {
|
|
389
|
+
width: 16px;
|
|
390
|
+
height: 16px;
|
|
391
|
+
border: 3px solid #d9d5ca;
|
|
392
|
+
border-top-color: #1d6154;
|
|
393
|
+
border-radius: 50%;
|
|
394
|
+
animation: loading-spin 0.7s linear infinite;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.wallet-option--skeleton {
|
|
398
|
+
pointer-events: none;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.wallet-skeleton__icon,
|
|
402
|
+
.wallet-skeleton__line {
|
|
403
|
+
border-radius: 8px;
|
|
404
|
+
background: #ece8dc;
|
|
405
|
+
animation: wallet-skeleton-pulse 1.2s ease-in-out infinite;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.wallet-skeleton__icon {
|
|
409
|
+
flex: none;
|
|
410
|
+
width: 36px;
|
|
411
|
+
height: 36px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.wallet-skeleton__line {
|
|
415
|
+
width: 45%;
|
|
416
|
+
height: 14px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@keyframes wallet-skeleton-pulse {
|
|
420
|
+
50% {
|
|
421
|
+
opacity: 0.55;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.wallet-modal__empty {
|
|
426
|
+
margin: 14px 10px;
|
|
427
|
+
color: #45515a;
|
|
428
|
+
font-size: 0.88rem;
|
|
429
|
+
line-height: 1.4;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.wallet-modal__error {
|
|
433
|
+
margin: 0 20px 4px;
|
|
434
|
+
max-height: 72px;
|
|
435
|
+
overflow-y: auto;
|
|
436
|
+
border: 1px solid #e7c8c8;
|
|
437
|
+
border-radius: 8px;
|
|
438
|
+
padding: 8px 10px;
|
|
439
|
+
background: #fbf1f1;
|
|
440
|
+
color: #8b1e1e;
|
|
441
|
+
font-size: 0.82rem;
|
|
442
|
+
line-height: 1.4;
|
|
443
|
+
white-space: pre-wrap;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.wallet-modal__footer {
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
justify-content: space-between;
|
|
450
|
+
gap: 8px;
|
|
451
|
+
padding: 14px 20px;
|
|
452
|
+
border-top: 1px solid #eee9dd;
|
|
453
|
+
color: #697076;
|
|
454
|
+
font-size: 0.82rem;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.wallet-modal__footer a {
|
|
458
|
+
color: #1d6154;
|
|
459
|
+
font-weight: 700;
|
|
460
|
+
text-decoration: none;
|
|
461
|
+
white-space: nowrap;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.wallet-modal__footer a:hover {
|
|
465
|
+
text-decoration: underline;
|
|
466
|
+
}
|
|
467
|
+
|
|
242
468
|
@media (max-width: 560px) {
|
|
243
469
|
.topbar,
|
|
244
470
|
.counter-panel__header {
|
|
@@ -254,4 +480,39 @@ button:hover {
|
|
|
254
480
|
width: 100%;
|
|
255
481
|
justify-content: center;
|
|
256
482
|
}
|
|
483
|
+
|
|
484
|
+
.wallet-modal {
|
|
485
|
+
align-items: flex-end;
|
|
486
|
+
padding: 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.wallet-modal__panel {
|
|
490
|
+
width: 100%;
|
|
491
|
+
max-height: 80vh;
|
|
492
|
+
border-bottom: 0;
|
|
493
|
+
border-radius: 16px 16px 0 0;
|
|
494
|
+
animation: wallet-modal-rise 0.2s ease;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@keyframes wallet-modal-rise {
|
|
499
|
+
from {
|
|
500
|
+
opacity: 0;
|
|
501
|
+
transform: translateY(24px);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@media (prefers-reduced-motion: reduce) {
|
|
506
|
+
.wallet-modal__panel {
|
|
507
|
+
animation: none;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.wallet-option__spinner {
|
|
511
|
+
animation-duration: 1.6s;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.wallet-skeleton__icon,
|
|
515
|
+
.wallet-skeleton__line {
|
|
516
|
+
animation: none;
|
|
517
|
+
}
|
|
257
518
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Promise bridge between the wallet adapter's `openModal()` capability and the
|
|
2
|
+
// <WalletModal> component. The adapter resolves the same promise the wallet
|
|
3
|
+
// session awaits, so connect/persist/restore flows stay untouched.
|
|
4
|
+
|
|
5
|
+
export interface WalletModalState {
|
|
6
|
+
open: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `error.name` used when the user closes the modal without picking a wallet.
|
|
11
|
+
* Closing on purpose is not a failure, so the UI can suppress this one.
|
|
12
|
+
*/
|
|
13
|
+
export const WALLET_SELECTION_CLOSED_ERROR = "WalletSelectionClosedError";
|
|
14
|
+
|
|
15
|
+
interface PendingSelection {
|
|
16
|
+
resolve(address: string): void;
|
|
17
|
+
reject(error: Error): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let state: WalletModalState = { open: false };
|
|
21
|
+
let pending: PendingSelection | null = null;
|
|
22
|
+
const listeners = new Set<() => void>();
|
|
23
|
+
|
|
24
|
+
function setState(next: WalletModalState): void {
|
|
25
|
+
state = next;
|
|
26
|
+
for (const listener of listeners) {
|
|
27
|
+
listener();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getWalletModalState(): WalletModalState {
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function subscribeWalletModal(listener: () => void): () => void {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
return () => {
|
|
38
|
+
listeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Opens the modal. Resolves with the connected address, rejects when closed. */
|
|
43
|
+
export function requestWalletSelection(): Promise<string> {
|
|
44
|
+
// A connect while the modal is already open supersedes the previous request.
|
|
45
|
+
cancelWalletSelection();
|
|
46
|
+
|
|
47
|
+
return new Promise<string>((resolve, reject) => {
|
|
48
|
+
pending = { resolve, reject };
|
|
49
|
+
setState({ open: true });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveWalletSelection(address: string): void {
|
|
54
|
+
const current = pending;
|
|
55
|
+
pending = null;
|
|
56
|
+
if (state.open) {
|
|
57
|
+
setState({ open: false });
|
|
58
|
+
}
|
|
59
|
+
current?.resolve(address);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cancelWalletSelection(): void {
|
|
63
|
+
const current = pending;
|
|
64
|
+
pending = null;
|
|
65
|
+
if (state.open) {
|
|
66
|
+
setState({ open: false });
|
|
67
|
+
}
|
|
68
|
+
if (current) {
|
|
69
|
+
const error = new Error("Wallet selection closed.");
|
|
70
|
+
error.name = WALLET_SELECTION_CLOSED_ERROR;
|
|
71
|
+
current.reject(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -4,12 +4,20 @@ import {
|
|
|
4
4
|
WalletNetwork,
|
|
5
5
|
type StellarWalletsKitMetadata
|
|
6
6
|
} from "@caatinga/client/stellar-wallets-kit";
|
|
7
|
+
import { requestWalletSelection } from "./wallet-modal-controller.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const baseWalletAdapter = createStellarWalletsKitAdapter({
|
|
9
10
|
network: WalletNetwork.TESTNET,
|
|
10
11
|
walletConnectMetadata: getWalletConnectMetadata()
|
|
11
12
|
});
|
|
12
13
|
|
|
14
|
+
// Route connect() through the custom <WalletModal> instead of SWK's built-in
|
|
15
|
+
// authModal. Everything else (persistence, restore, signing) is untouched.
|
|
16
|
+
export const stellarWalletAdapter = {
|
|
17
|
+
...baseWalletAdapter,
|
|
18
|
+
openModal: () => requestWalletSelection()
|
|
19
|
+
};
|
|
20
|
+
|
|
13
21
|
export { WalletNetwork };
|
|
14
22
|
|
|
15
23
|
function getWalletConnectMetadata(): StellarWalletsKitMetadata | undefined {
|
|
@@ -2,6 +2,14 @@ import { defineConfig } from "vite";
|
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
+
// Empty CJS module. Aliasing browser-hostile wallet sub-deps here keeps them out
|
|
6
|
+
// of the bundle on every package manager — no install-time dependency overrides
|
|
7
|
+
// (which differ per pnpm/npm/yarn/bun) required. CJS interop lets any named
|
|
8
|
+
// import resolve to `undefined` without an esbuild "not exported" error.
|
|
9
|
+
const emptyStub = fileURLToPath(
|
|
10
|
+
new URL("./src/stubs/empty-wallet-dep/index.cjs", import.meta.url)
|
|
11
|
+
);
|
|
12
|
+
|
|
5
13
|
export default defineConfig({
|
|
6
14
|
plugins: [react()],
|
|
7
15
|
resolve: {
|
|
@@ -9,7 +17,15 @@ export default defineConfig({
|
|
|
9
17
|
// Stellar Wallets Kit drags NEAR's @hot-wallet/sdk (Node-only crypto) into
|
|
10
18
|
// the browser bundle. The adapter filters HOT Wallet out, so stub the SDK
|
|
11
19
|
// to keep the NEAR chain out of the build. See src/stubs/hot-wallet.ts.
|
|
12
|
-
"@hot-wallet/sdk": fileURLToPath(new URL("./src/stubs/hot-wallet.ts", import.meta.url))
|
|
20
|
+
"@hot-wallet/sdk": fileURLToPath(new URL("./src/stubs/hot-wallet.ts", import.meta.url)),
|
|
21
|
+
// SWK + Reown/WalletConnect pull Trezor Connect (Node-only) and Safe Global
|
|
22
|
+
// SDKs that none of the wallets Caatinga ships actually use. Stub them so the
|
|
23
|
+
// bundle builds without pnpm-only "-" dependency overrides.
|
|
24
|
+
"@trezor/connect-web": emptyStub,
|
|
25
|
+
"@trezor/connect-plugin-stellar": emptyStub,
|
|
26
|
+
"@safe-global/safe-apps-sdk": emptyStub,
|
|
27
|
+
"@safe-global/safe-apps-provider": emptyStub,
|
|
28
|
+
"@safe-global/safe-gateway-typescript-sdk": emptyStub
|
|
13
29
|
}
|
|
14
30
|
}
|
|
15
31
|
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
useCallback,
|
|
4
|
-
useContext,
|
|
5
|
-
useMemo,
|
|
6
|
-
useState,
|
|
7
|
-
type ReactNode
|
|
8
|
-
} from "react";
|
|
9
|
-
import { stellarWalletAdapter } from "../wallet.js";
|
|
10
|
-
|
|
11
|
-
interface WalletContextValue {
|
|
12
|
-
publicKey: string | null;
|
|
13
|
-
loading: boolean;
|
|
14
|
-
error: string | null;
|
|
15
|
-
connect: () => Promise<void>;
|
|
16
|
-
disconnect: () => Promise<void>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const WalletContext = createContext<WalletContextValue | null>(null);
|
|
20
|
-
|
|
21
|
-
export function WalletProvider({ children }: { children: ReactNode }) {
|
|
22
|
-
const [publicKey, setPublicKey] = useState<string | null>(null);
|
|
23
|
-
const [loading, setLoading] = useState(false);
|
|
24
|
-
const [error, setError] = useState<string | null>(null);
|
|
25
|
-
|
|
26
|
-
const connect = useCallback(async () => {
|
|
27
|
-
setLoading(true);
|
|
28
|
-
setError(null);
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
// openModal lists only installed/available wallets and resolves with the
|
|
32
|
-
// chosen account address (rejects if the user dismisses the modal).
|
|
33
|
-
const address = await stellarWalletAdapter.openModal();
|
|
34
|
-
setPublicKey(address);
|
|
35
|
-
} catch (caught) {
|
|
36
|
-
const message = caught instanceof Error ? caught.message : String(caught);
|
|
37
|
-
setError(message);
|
|
38
|
-
} finally {
|
|
39
|
-
setLoading(false);
|
|
40
|
-
}
|
|
41
|
-
}, []);
|
|
42
|
-
|
|
43
|
-
const disconnect = useCallback(async () => {
|
|
44
|
-
await stellarWalletAdapter.disconnect();
|
|
45
|
-
setPublicKey(null);
|
|
46
|
-
setError(null);
|
|
47
|
-
}, []);
|
|
48
|
-
|
|
49
|
-
const value = useMemo<WalletContextValue>(
|
|
50
|
-
() => ({ publicKey, loading, error, connect, disconnect }),
|
|
51
|
-
[publicKey, loading, error, connect, disconnect]
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
return <WalletContext.Provider value={value}>{children}</WalletContext.Provider>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function useWallet(): WalletContextValue {
|
|
58
|
-
const context = useContext(WalletContext);
|
|
59
|
-
if (!context) {
|
|
60
|
-
throw new Error("useWallet must be used within a WalletProvider");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return context;
|
|
64
|
-
}
|