stellar-agent 0.1.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 +162 -0
- package/package.json +37 -0
- package/src/core-skills/module-help.csv +5 -0
- package/src/core-skills/module.yaml +33 -0
- package/src/core-skills/stellar-brainstorming/SKILL.md +6 -0
- package/src/core-skills/stellar-brainstorming/steps/step-01-session-setup.md +67 -0
- package/src/core-skills/stellar-brainstorming/steps/step-02a-user-selected.md +20 -0
- package/src/core-skills/stellar-brainstorming/steps/step-02b-ai-recommended.md +29 -0
- package/src/core-skills/stellar-brainstorming/steps/step-03-technique-execution.md +69 -0
- package/src/core-skills/stellar-brainstorming/steps/step-04-idea-organization.md +64 -0
- package/src/core-skills/stellar-brainstorming/workflow.md +50 -0
- package/src/core-skills/stellar-help/SKILL.md +71 -0
- package/src/core-skills/stellar-party-mode/SKILL.md +109 -0
- package/src/scripts/resolve_config.py +170 -0
- package/src/scripts/resolve_customization.py +209 -0
- package/src/stellar-skills/1-analysis/stellar-agent-analyst/SKILL.md +71 -0
- package/src/stellar-skills/1-analysis/stellar-agent-analyst/customize.toml +41 -0
- package/src/stellar-skills/1-analysis/stellar-analytics/SKILL.md +239 -0
- package/src/stellar-skills/1-analysis/stellar-domain-research/SKILL.md +82 -0
- package/src/stellar-skills/1-analysis/stellar-market-research/SKILL.md +90 -0
- package/src/stellar-skills/2-planning/stellar-agent-pm/SKILL.md +57 -0
- package/src/stellar-skills/2-planning/stellar-agent-pm/customize.toml +36 -0
- package/src/stellar-skills/2-planning/stellar-epics-stories/SKILL.md +106 -0
- package/src/stellar-skills/2-planning/stellar-prd/SKILL.md +115 -0
- package/src/stellar-skills/2-planning/stellar-project-brief/SKILL.md +83 -0
- package/src/stellar-skills/3-architecture/stellar-agent-architect/SKILL.md +53 -0
- package/src/stellar-skills/3-architecture/stellar-agent-architect/customize.toml +31 -0
- package/src/stellar-skills/3-architecture/stellar-architecture-doc/SKILL.md +162 -0
- package/src/stellar-skills/4-implementation/stellar-agent-developer/SKILL.md +54 -0
- package/src/stellar-skills/4-implementation/stellar-agent-developer/customize.toml +56 -0
- package/src/stellar-skills/4-implementation/stellar-agent-devops/SKILL.md +54 -0
- package/src/stellar-skills/4-implementation/stellar-agent-devops/customize.toml +36 -0
- package/src/stellar-skills/4-implementation/stellar-agent-frontend/SKILL.md +54 -0
- package/src/stellar-skills/4-implementation/stellar-agent-frontend/customize.toml +52 -0
- package/src/stellar-skills/4-implementation/stellar-agent-qa/SKILL.md +54 -0
- package/src/stellar-skills/4-implementation/stellar-agent-qa/customize.toml +31 -0
- package/src/stellar-skills/4-implementation/stellar-create-asset/SKILL.md +145 -0
- package/src/stellar-skills/4-implementation/stellar-create-transaction/SKILL.md +134 -0
- package/src/stellar-skills/4-implementation/stellar-deploy-contract/SKILL.md +124 -0
- package/src/stellar-skills/4-implementation/stellar-freighter-integration/SKILL.md +193 -0
- package/src/stellar-skills/4-implementation/stellar-horizon-integration/SKILL.md +198 -0
- package/src/stellar-skills/4-implementation/stellar-init-contract/SKILL.md +102 -0
- package/src/stellar-skills/4-implementation/stellar-liquidity-pool/SKILL.md +156 -0
- package/src/stellar-skills/4-implementation/stellar-nextjs-setup/SKILL.md +198 -0
- package/src/stellar-skills/4-implementation/stellar-nextjs-soroban/SKILL.md +228 -0
- package/src/stellar-skills/4-implementation/stellar-nextjs-wallet/SKILL.md +276 -0
- package/src/stellar-skills/4-implementation/stellar-sep10-auth/SKILL.md +252 -0
- package/src/stellar-skills/4-implementation/stellar-setup-environment/SKILL.md +163 -0
- package/src/stellar-skills/4-implementation/stellar-setup-trustline/SKILL.md +107 -0
- package/src/stellar-skills/4-implementation/stellar-test-contract/SKILL.md +146 -0
- package/src/stellar-skills/4-implementation/stellar-write-contract/SKILL.md +140 -0
- package/src/stellar-skills/module-help.csv +24 -0
- package/src/stellar-skills/module.yaml +103 -0
- package/tools/installer/cli-utils.js +39 -0
- package/tools/installer/commands/init.js +335 -0
- package/tools/installer/fs-native.js +116 -0
- package/tools/installer/prompts.js +852 -0
- package/tools/installer/stellar-cli.js +80 -0
- package/tools/installer/yaml-format.js +245 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stellar-nextjs-setup
|
|
3
|
+
description: 'Scaffold a Next.js App Router project with Stellar SDK, environment configuration, and network setup. Use when the user wants to build a Stellar dApp frontend with Next.js.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next.js + Stellar dApp Setup
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Bootstrap a production-ready Next.js App Router project wired up to the Stellar network.
|
|
11
|
+
|
|
12
|
+
## On Activation
|
|
13
|
+
|
|
14
|
+
Load `{network_preference}` and `{project_name}` from `{project-root}/_stellar/stellar/config.yaml`.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
### Step 1: Create Next.js Project
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx create-next-app@latest {project_name} \
|
|
22
|
+
--typescript \
|
|
23
|
+
--tailwind \
|
|
24
|
+
--eslint \
|
|
25
|
+
--app \
|
|
26
|
+
--src-dir \
|
|
27
|
+
--import-alias "@/*"
|
|
28
|
+
cd {project_name}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Step 2: Install Stellar Dependencies
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install stellar-sdk @stellar/freighter-api bignumber.js
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For the TypeScript Wallet SDK (higher-level wallet operations):
|
|
38
|
+
```bash
|
|
39
|
+
npm install @stellar/typescript-wallet-sdk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Step 3: Configure Environment Variables
|
|
43
|
+
|
|
44
|
+
Create `.env.local`:
|
|
45
|
+
|
|
46
|
+
```env
|
|
47
|
+
# Network
|
|
48
|
+
NEXT_PUBLIC_STELLAR_NETWORK=testnet
|
|
49
|
+
NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
|
|
50
|
+
NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org
|
|
51
|
+
NEXT_PUBLIC_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
|
|
52
|
+
|
|
53
|
+
# Contract IDs (fill in after deployment)
|
|
54
|
+
NEXT_PUBLIC_CONTRACT_ID=
|
|
55
|
+
|
|
56
|
+
# For mainnet, override:
|
|
57
|
+
# NEXT_PUBLIC_STELLAR_NETWORK=mainnet
|
|
58
|
+
# NEXT_PUBLIC_HORIZON_URL=https://horizon.stellar.org
|
|
59
|
+
# NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-mainnet.stellar.org
|
|
60
|
+
# NEXT_PUBLIC_NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Create `.env.local.example` (commit this, not `.env.local`):
|
|
64
|
+
```env
|
|
65
|
+
NEXT_PUBLIC_STELLAR_NETWORK=testnet
|
|
66
|
+
NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
|
|
67
|
+
NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org
|
|
68
|
+
NEXT_PUBLIC_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
|
|
69
|
+
NEXT_PUBLIC_CONTRACT_ID=
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Add to `.gitignore`:
|
|
73
|
+
```
|
|
74
|
+
.env.local
|
|
75
|
+
.env.*.local
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Step 4: Create Network Config Module
|
|
79
|
+
|
|
80
|
+
`src/lib/stellar/config.ts`:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
export const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK || 'testnet';
|
|
84
|
+
export const HORIZON_URL = process.env.NEXT_PUBLIC_HORIZON_URL!;
|
|
85
|
+
export const STELLAR_RPC_URL = process.env.NEXT_PUBLIC_STELLAR_RPC_URL!;
|
|
86
|
+
export const NETWORK_PASSPHRASE = process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE!;
|
|
87
|
+
export const CONTRACT_ID = process.env.NEXT_PUBLIC_CONTRACT_ID!;
|
|
88
|
+
|
|
89
|
+
export function isMainnet() {
|
|
90
|
+
return NETWORK === 'mainnet';
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Step 5: Create Horizon Server Instance
|
|
95
|
+
|
|
96
|
+
`src/lib/stellar/horizon.ts`:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { Horizon } from 'stellar-sdk';
|
|
100
|
+
import { HORIZON_URL } from './config';
|
|
101
|
+
|
|
102
|
+
export const horizonServer = new Horizon.Server(HORIZON_URL, {
|
|
103
|
+
allowHttp: HORIZON_URL.startsWith('http://'), // allow for local dev
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Step 6: Create Soroban RPC Client
|
|
108
|
+
|
|
109
|
+
`src/lib/stellar/soroban.ts`:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { SorobanRpc } from 'stellar-sdk';
|
|
113
|
+
import { STELLAR_RPC_URL } from './config';
|
|
114
|
+
|
|
115
|
+
export const rpcServer = new SorobanRpc.Server(STELLAR_RPC_URL, {
|
|
116
|
+
allowHttp: STELLAR_RPC_URL.startsWith('http://'),
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Step 7: Project Structure
|
|
121
|
+
|
|
122
|
+
Recommended folder layout:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
src/
|
|
126
|
+
app/
|
|
127
|
+
page.tsx # home page
|
|
128
|
+
layout.tsx # root layout with providers
|
|
129
|
+
globals.css
|
|
130
|
+
components/
|
|
131
|
+
stellar/
|
|
132
|
+
WalletButton.tsx # connect/disconnect Freighter
|
|
133
|
+
AccountBalance.tsx # display XLM and token balances
|
|
134
|
+
TransactionStatus.tsx
|
|
135
|
+
lib/
|
|
136
|
+
stellar/
|
|
137
|
+
config.ts # network config
|
|
138
|
+
horizon.ts # Horizon server
|
|
139
|
+
soroban.ts # Soroban RPC client
|
|
140
|
+
utils.ts # shared helpers (stroops ↔ XLM, etc.)
|
|
141
|
+
hooks/
|
|
142
|
+
useFreighter.ts # wallet state hook
|
|
143
|
+
useAccount.ts # account data from Horizon
|
|
144
|
+
types/
|
|
145
|
+
stellar.ts # shared TypeScript types
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Step 8: Utility Helpers
|
|
149
|
+
|
|
150
|
+
`src/lib/stellar/utils.ts`:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import BigNumber from 'bignumber.js';
|
|
154
|
+
|
|
155
|
+
// Convert stroops (integer) to XLM (decimal string)
|
|
156
|
+
export function stroopsToXlm(stroops: string | number): string {
|
|
157
|
+
return new BigNumber(stroops).dividedBy(1e7).toFixed(7);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Convert XLM to stroops
|
|
161
|
+
export function xlmToStroops(xlm: string | number): string {
|
|
162
|
+
return new BigNumber(xlm).multipliedBy(1e7).integerValue().toFixed();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Shorten a Stellar public key for display
|
|
166
|
+
export function shortenKey(key: string, chars = 4): string {
|
|
167
|
+
return `${key.slice(0, chars + 1)}...${key.slice(-chars)}`;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Step 9: TypeScript Types
|
|
172
|
+
|
|
173
|
+
`src/types/stellar.ts`:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
export type StellarNetwork = 'testnet' | 'mainnet' | 'local';
|
|
177
|
+
|
|
178
|
+
export interface AccountInfo {
|
|
179
|
+
publicKey: string;
|
|
180
|
+
balances: {
|
|
181
|
+
asset: string;
|
|
182
|
+
balance: string;
|
|
183
|
+
}[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface ContractInvokeResult {
|
|
187
|
+
success: boolean;
|
|
188
|
+
value?: unknown;
|
|
189
|
+
error?: string;
|
|
190
|
+
txHash?: string;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Next Steps
|
|
195
|
+
|
|
196
|
+
- `stellar-nextjs-wallet` — add Freighter wallet connect/disconnect
|
|
197
|
+
- `stellar-nextjs-soroban` — call Soroban contracts from Next.js
|
|
198
|
+
- `stellar-nextjs-horizon` via `stellar-horizon-integration` — fetch account and transaction data
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stellar-nextjs-soroban
|
|
3
|
+
description: 'Call Soroban smart contracts from a Next.js App Router project using Server Actions, API routes, and client-side invocation with Freighter signing. Use when the user wants to invoke Soroban contracts from their Next.js Stellar dApp.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next.js + Soroban Contract Integration
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Connect a Next.js App Router frontend to deployed Soroban contracts — covering read-only queries (Server Components / API routes) and signed mutations (client-side with Freighter).
|
|
11
|
+
|
|
12
|
+
## On Activation
|
|
13
|
+
|
|
14
|
+
Load `{network_preference}` and `{primary_language}` from `{project-root}/_stellar/stellar/config.yaml`. Confirm `stellar-nextjs-setup` and `stellar-nextjs-wallet` are complete.
|
|
15
|
+
|
|
16
|
+
## Key Distinction
|
|
17
|
+
|
|
18
|
+
| Operation | Where to run | Why |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Read-only query (no auth) | Server Component or API route | No wallet needed; can cache |
|
|
21
|
+
| Write / mutation | Client Component (needs Freighter) | Must sign with user's key |
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
### Step 1: Generate TypeScript Bindings (Recommended)
|
|
26
|
+
|
|
27
|
+
If the contract has an interface, generate typed bindings:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
stellar contract bindings typescript \
|
|
31
|
+
--network {network_preference} \
|
|
32
|
+
--id {contract_id} \
|
|
33
|
+
--output-dir src/lib/stellar/contracts/{contract_name}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This generates a fully-typed client — use it instead of raw `SorobanRpc` calls.
|
|
37
|
+
|
|
38
|
+
### Step 2: Read-Only Query in a Server Component
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// src/app/dashboard/page.tsx (Server Component — no "use client")
|
|
42
|
+
import { SorobanRpc, Contract, scValToNative, xdr } from 'stellar-sdk';
|
|
43
|
+
import { rpcServer } from '@/lib/stellar/soroban';
|
|
44
|
+
import { CONTRACT_ID, NETWORK_PASSPHRASE } from '@/lib/stellar/config';
|
|
45
|
+
|
|
46
|
+
async function getContractState(): Promise<number> {
|
|
47
|
+
const contract = new Contract(CONTRACT_ID);
|
|
48
|
+
const tx = contract.call('get_count'); // no-auth view function
|
|
49
|
+
|
|
50
|
+
const simResult = await rpcServer.simulateTransaction(tx as any);
|
|
51
|
+
if (SorobanRpc.Api.isSimulationError(simResult)) {
|
|
52
|
+
throw new Error(simResult.error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const returnVal = (simResult as SorobanRpc.Api.SimulateTransactionSuccessResponse)
|
|
56
|
+
.result?.retval;
|
|
57
|
+
return returnVal ? (scValToNative(returnVal) as number) : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default async function DashboardPage() {
|
|
61
|
+
const count = await getContractState();
|
|
62
|
+
return <div>Count: {count}</div>;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Step 3: Read-Only Query via API Route (for client-side use)
|
|
67
|
+
|
|
68
|
+
`src/app/api/contract/count/route.ts`:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { NextResponse } from 'next/server';
|
|
72
|
+
import { SorobanRpc, Contract, scValToNative } from 'stellar-sdk';
|
|
73
|
+
import { rpcServer } from '@/lib/stellar/soroban';
|
|
74
|
+
import { CONTRACT_ID } from '@/lib/stellar/config';
|
|
75
|
+
|
|
76
|
+
export async function GET() {
|
|
77
|
+
try {
|
|
78
|
+
const contract = new Contract(CONTRACT_ID);
|
|
79
|
+
const tx = contract.call('get_count');
|
|
80
|
+
const simResult = await rpcServer.simulateTransaction(tx as any);
|
|
81
|
+
|
|
82
|
+
if (SorobanRpc.Api.isSimulationError(simResult)) {
|
|
83
|
+
return NextResponse.json({ error: simResult.error }, { status: 500 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const retval = (simResult as SorobanRpc.Api.SimulateTransactionSuccessResponse)
|
|
87
|
+
.result?.retval;
|
|
88
|
+
const count = retval ? scValToNative(retval) : 0;
|
|
89
|
+
return NextResponse.json({ count });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Step 4: Signed Contract Call (Client Component)
|
|
97
|
+
|
|
98
|
+
For functions that require auth (user must sign with Freighter):
|
|
99
|
+
|
|
100
|
+
`src/components/stellar/IncrementButton.tsx`:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
"use client";
|
|
104
|
+
|
|
105
|
+
import { useState } from 'react';
|
|
106
|
+
import {
|
|
107
|
+
SorobanRpc,
|
|
108
|
+
TransactionBuilder,
|
|
109
|
+
Contract,
|
|
110
|
+
Networks,
|
|
111
|
+
nativeToScVal,
|
|
112
|
+
} from 'stellar-sdk';
|
|
113
|
+
import { rpcServer } from '@/lib/stellar/soroban';
|
|
114
|
+
import { horizonServer } from '@/lib/stellar/horizon';
|
|
115
|
+
import { useWallet } from '@/contexts/WalletContext';
|
|
116
|
+
import { CONTRACT_ID, NETWORK_PASSPHRASE } from '@/lib/stellar/config';
|
|
117
|
+
|
|
118
|
+
export function IncrementButton() {
|
|
119
|
+
const { publicKey, sign, connected } = useWallet();
|
|
120
|
+
const [status, setStatus] = useState<string>('');
|
|
121
|
+
|
|
122
|
+
async function handleIncrement() {
|
|
123
|
+
if (!publicKey) return;
|
|
124
|
+
setStatus('Building transaction...');
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// 1. Load account from Horizon for sequence number
|
|
128
|
+
const account = await horizonServer.loadAccount(publicKey);
|
|
129
|
+
|
|
130
|
+
// 2. Build the contract call transaction
|
|
131
|
+
const contract = new Contract(CONTRACT_ID);
|
|
132
|
+
const tx = new TransactionBuilder(account, {
|
|
133
|
+
fee: '100',
|
|
134
|
+
networkPassphrase: NETWORK_PASSPHRASE,
|
|
135
|
+
})
|
|
136
|
+
.addOperation(contract.call('increment'))
|
|
137
|
+
.setTimeout(30)
|
|
138
|
+
.build();
|
|
139
|
+
|
|
140
|
+
// 3. Simulate to get auth + resource estimates
|
|
141
|
+
setStatus('Simulating...');
|
|
142
|
+
const simResult = await rpcServer.simulateTransaction(tx);
|
|
143
|
+
if (SorobanRpc.Api.isSimulationError(simResult)) {
|
|
144
|
+
throw new Error(simResult.error);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 4. Assemble (inject auth + footprint from simulation)
|
|
148
|
+
const preparedTx = SorobanRpc.assembleTransaction(tx, simResult).build();
|
|
149
|
+
|
|
150
|
+
// 5. Sign with Freighter
|
|
151
|
+
setStatus('Waiting for signature...');
|
|
152
|
+
const signedXdr = await sign(preparedTx.toXDR());
|
|
153
|
+
const signedTx = TransactionBuilder.fromXDR(signedXdr, NETWORK_PASSPHRASE);
|
|
154
|
+
|
|
155
|
+
// 6. Submit
|
|
156
|
+
setStatus('Submitting...');
|
|
157
|
+
const sendResult = await rpcServer.sendTransaction(signedTx);
|
|
158
|
+
if (sendResult.status === 'ERROR') {
|
|
159
|
+
throw new Error(sendResult.errorResult?.toString());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 7. Poll for confirmation
|
|
163
|
+
let getResult = await rpcServer.getTransaction(sendResult.hash);
|
|
164
|
+
while (getResult.status === SorobanRpc.Api.GetTransactionStatus.NOT_FOUND) {
|
|
165
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
166
|
+
getResult = await rpcServer.getTransaction(sendResult.hash);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (getResult.status === SorobanRpc.Api.GetTransactionStatus.SUCCESS) {
|
|
170
|
+
setStatus(`Done! Tx: ${sendResult.hash.slice(0, 8)}...`);
|
|
171
|
+
} else {
|
|
172
|
+
throw new Error('Transaction failed');
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
setStatus(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!connected) return <p>Connect wallet to interact with contract</p>;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div>
|
|
183
|
+
<button onClick={handleIncrement} className="btn btn-primary">
|
|
184
|
+
Increment
|
|
185
|
+
</button>
|
|
186
|
+
{status && <p className="text-sm mt-2">{status}</p>}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Step 5: Working with ScVal Types
|
|
193
|
+
|
|
194
|
+
Common conversions between JavaScript values and Soroban's `xdr.ScVal`:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { nativeToScVal, scValToNative, xdr } from 'stellar-sdk';
|
|
198
|
+
|
|
199
|
+
// JS → ScVal
|
|
200
|
+
nativeToScVal(42, { type: 'u32' }) // u32
|
|
201
|
+
nativeToScVal('hello') // string/symbol
|
|
202
|
+
nativeToScVal(publicKey, { type: 'address' }) // Address
|
|
203
|
+
nativeToScVal(true) // bool
|
|
204
|
+
|
|
205
|
+
// ScVal → JS
|
|
206
|
+
const value = scValToNative(scVal); // auto-detects type
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Step 6: Fee Strategy
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const feeStats = await horizonServer.feeStats();
|
|
213
|
+
const fee = feeStats.fee_charged.p75; // p75 for reliable inclusion
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Step 7: Error Handling
|
|
217
|
+
|
|
218
|
+
| Scenario | Check |
|
|
219
|
+
|---|---|
|
|
220
|
+
| Simulation failed | `SorobanRpc.Api.isSimulationError(result)` |
|
|
221
|
+
| Contract panic / `Error::` | `simResult.error` string contains contract error name |
|
|
222
|
+
| Transaction not found after submit | Poll `getTransaction` with timeout |
|
|
223
|
+
| Auth required but not provided | Contract returns auth error — check contract ABI |
|
|
224
|
+
|
|
225
|
+
### Next Steps
|
|
226
|
+
|
|
227
|
+
- `stellar-test-contract` — write Rust tests before wiring up the frontend
|
|
228
|
+
- `stellar-sep10-auth` — add JWT auth for API routes that need user identity
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stellar-nextjs-wallet
|
|
3
|
+
description: 'Integrate Freighter wallet into a Next.js App Router project with connect/disconnect, signing, and account state management. Use when the user wants wallet functionality in their Next.js Stellar dApp.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next.js Wallet Integration (Freighter)
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Add Freighter wallet connect/disconnect, account state, and transaction signing to a Next.js App Router project.
|
|
11
|
+
|
|
12
|
+
## On Activation
|
|
13
|
+
|
|
14
|
+
Load `{network_preference}` from `{project-root}/_stellar/stellar/config.yaml`. Verify `@stellar/freighter-api` is installed.
|
|
15
|
+
|
|
16
|
+
## Core Concept
|
|
17
|
+
|
|
18
|
+
Freighter is a browser extension. All Freighter API calls must run **client-side only** (inside `"use client"` components or hooks). Never call Freighter APIs in Server Components or server-side code.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
### Step 1: Install Freighter API
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @stellar/freighter-api
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Step 2: Wallet State Hook
|
|
29
|
+
|
|
30
|
+
`src/hooks/useFreighter.ts`:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
"use client";
|
|
34
|
+
|
|
35
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
36
|
+
import {
|
|
37
|
+
isConnected,
|
|
38
|
+
getPublicKey,
|
|
39
|
+
signTransaction,
|
|
40
|
+
setAllowed,
|
|
41
|
+
isAllowed,
|
|
42
|
+
getNetworkDetails,
|
|
43
|
+
} from '@stellar/freighter-api';
|
|
44
|
+
import { NETWORK_PASSPHRASE, NETWORK } from '@/lib/stellar/config';
|
|
45
|
+
|
|
46
|
+
interface FreighterState {
|
|
47
|
+
connected: boolean;
|
|
48
|
+
publicKey: string | null;
|
|
49
|
+
loading: boolean;
|
|
50
|
+
error: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useFreighter() {
|
|
54
|
+
const [state, setState] = useState<FreighterState>({
|
|
55
|
+
connected: false,
|
|
56
|
+
publicKey: null,
|
|
57
|
+
loading: true,
|
|
58
|
+
error: null,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Check connection on mount
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
async function checkConnection() {
|
|
64
|
+
try {
|
|
65
|
+
const connected = await isConnected();
|
|
66
|
+
if (connected) {
|
|
67
|
+
const allowed = await isAllowed();
|
|
68
|
+
if (allowed) {
|
|
69
|
+
const publicKey = await getPublicKey();
|
|
70
|
+
setState({ connected: true, publicKey, loading: false, error: null });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
setState(s => ({ ...s, connected: false, publicKey: null, loading: false }));
|
|
75
|
+
} catch {
|
|
76
|
+
setState(s => ({ ...s, loading: false, error: 'Freighter not installed' }));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
checkConnection();
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const connect = useCallback(async () => {
|
|
83
|
+
setState(s => ({ ...s, loading: true, error: null }));
|
|
84
|
+
try {
|
|
85
|
+
await setAllowed();
|
|
86
|
+
const publicKey = await getPublicKey();
|
|
87
|
+
|
|
88
|
+
// Verify network matches
|
|
89
|
+
const networkDetails = await getNetworkDetails();
|
|
90
|
+
if (networkDetails.networkPassphrase !== NETWORK_PASSPHRASE) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Switch Freighter to ${NETWORK}. Current: ${networkDetails.networkName}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setState({ connected: true, publicKey, loading: false, error: null });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
setState(s => ({
|
|
99
|
+
...s,
|
|
100
|
+
loading: false,
|
|
101
|
+
error: err instanceof Error ? err.message : 'Connection failed',
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const disconnect = useCallback(() => {
|
|
107
|
+
setState({ connected: false, publicKey: null, loading: false, error: null });
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const sign = useCallback(
|
|
111
|
+
async (xdr: string): Promise<string> => {
|
|
112
|
+
if (!state.publicKey) throw new Error('Wallet not connected');
|
|
113
|
+
const result = await signTransaction(xdr, {
|
|
114
|
+
accountToSign: state.publicKey,
|
|
115
|
+
networkPassphrase: NETWORK_PASSPHRASE,
|
|
116
|
+
});
|
|
117
|
+
return result;
|
|
118
|
+
},
|
|
119
|
+
[state.publicKey]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return { ...state, connect, disconnect, sign };
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Step 3: Wallet Button Component
|
|
127
|
+
|
|
128
|
+
`src/components/stellar/WalletButton.tsx`:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
"use client";
|
|
132
|
+
|
|
133
|
+
import { useFreighter } from '@/hooks/useFreighter';
|
|
134
|
+
import { shortenKey } from '@/lib/stellar/utils';
|
|
135
|
+
|
|
136
|
+
export function WalletButton() {
|
|
137
|
+
const { connected, publicKey, loading, error, connect, disconnect } = useFreighter();
|
|
138
|
+
|
|
139
|
+
if (loading) {
|
|
140
|
+
return <button disabled className="btn btn-ghost">Loading...</button>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (error) {
|
|
144
|
+
return (
|
|
145
|
+
<button
|
|
146
|
+
onClick={() => window.open('https://www.freighter.app/', '_blank')}
|
|
147
|
+
className="btn btn-error btn-sm"
|
|
148
|
+
>
|
|
149
|
+
Install Freighter
|
|
150
|
+
</button>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (connected && publicKey) {
|
|
155
|
+
return (
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<span className="font-mono text-sm">{shortenKey(publicKey)}</span>
|
|
158
|
+
<button onClick={disconnect} className="btn btn-outline btn-sm">
|
|
159
|
+
Disconnect
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<button onClick={connect} className="btn btn-primary">
|
|
167
|
+
Connect Wallet
|
|
168
|
+
</button>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Step 4: Root Layout with Wallet Context (optional)
|
|
174
|
+
|
|
175
|
+
For apps that need wallet state across many components, add a React Context:
|
|
176
|
+
|
|
177
|
+
`src/contexts/WalletContext.tsx`:
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
"use client";
|
|
181
|
+
|
|
182
|
+
import { createContext, useContext, ReactNode } from 'react';
|
|
183
|
+
import { useFreighter } from '@/hooks/useFreighter';
|
|
184
|
+
|
|
185
|
+
type WalletContextType = ReturnType<typeof useFreighter>;
|
|
186
|
+
const WalletContext = createContext<WalletContextType | null>(null);
|
|
187
|
+
|
|
188
|
+
export function WalletProvider({ children }: { children: ReactNode }) {
|
|
189
|
+
const wallet = useFreighter();
|
|
190
|
+
return (
|
|
191
|
+
<WalletContext.Provider value={wallet}>{children}</WalletContext.Provider>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function useWallet() {
|
|
196
|
+
const ctx = useContext(WalletContext);
|
|
197
|
+
if (!ctx) throw new Error('useWallet must be used inside WalletProvider');
|
|
198
|
+
return ctx;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Wire into `src/app/layout.tsx`:
|
|
203
|
+
```tsx
|
|
204
|
+
import { WalletProvider } from '@/contexts/WalletContext';
|
|
205
|
+
|
|
206
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
207
|
+
return (
|
|
208
|
+
<html lang="en">
|
|
209
|
+
<body>
|
|
210
|
+
<WalletProvider>
|
|
211
|
+
{children}
|
|
212
|
+
</WalletProvider>
|
|
213
|
+
</body>
|
|
214
|
+
</html>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Step 5: Signing and Submitting a Transaction
|
|
220
|
+
|
|
221
|
+
Pattern for client components that need to send a transaction:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
"use client";
|
|
225
|
+
|
|
226
|
+
import { TransactionBuilder, Networks, Operation, Asset } from 'stellar-sdk';
|
|
227
|
+
import { horizonServer } from '@/lib/stellar/horizon';
|
|
228
|
+
import { useWallet } from '@/contexts/WalletContext';
|
|
229
|
+
import { NETWORK_PASSPHRASE } from '@/lib/stellar/config';
|
|
230
|
+
|
|
231
|
+
export function SendPaymentForm() {
|
|
232
|
+
const { publicKey, sign, connected } = useWallet();
|
|
233
|
+
|
|
234
|
+
async function handleSend(destination: string, amount: string) {
|
|
235
|
+
if (!publicKey) return;
|
|
236
|
+
|
|
237
|
+
const account = await horizonServer.loadAccount(publicKey);
|
|
238
|
+
const tx = new TransactionBuilder(account, {
|
|
239
|
+
fee: '100',
|
|
240
|
+
networkPassphrase: NETWORK_PASSPHRASE,
|
|
241
|
+
})
|
|
242
|
+
.addOperation(
|
|
243
|
+
Operation.payment({
|
|
244
|
+
destination,
|
|
245
|
+
asset: Asset.native(),
|
|
246
|
+
amount,
|
|
247
|
+
})
|
|
248
|
+
)
|
|
249
|
+
.setTimeout(30)
|
|
250
|
+
.build();
|
|
251
|
+
|
|
252
|
+
const signedXdr = await sign(tx.toXDR());
|
|
253
|
+
const signedTx = TransactionBuilder.fromXDR(signedXdr, NETWORK_PASSPHRASE);
|
|
254
|
+
const result = await horizonServer.submitTransaction(signedTx);
|
|
255
|
+
console.log('Submitted:', result.hash);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!connected) return <p>Connect wallet first</p>;
|
|
259
|
+
// ... render form
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Step 6: Edge Cases to Handle
|
|
264
|
+
|
|
265
|
+
| Case | Handling |
|
|
266
|
+
|---|---|
|
|
267
|
+
| Freighter not installed | Show "Install Freighter" link to `freighter.app` |
|
|
268
|
+
| Wrong network selected | Check `networkPassphrase` on connect; show error |
|
|
269
|
+
| User rejects transaction | Catch error from `signTransaction`, show toast |
|
|
270
|
+
| Account not funded | Catch `400` from Horizon submit, check `result_codes` |
|
|
271
|
+
| Freighter locked | `getPublicKey()` throws — prompt user to unlock |
|
|
272
|
+
|
|
273
|
+
### Next Steps
|
|
274
|
+
|
|
275
|
+
- `stellar-nextjs-soroban` — call Soroban contracts using the connected wallet
|
|
276
|
+
- `stellar-sep10-auth` — add SEP-10 JWT auth for backend calls
|