x402-engineer 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/AGENT.md +102 -0
- package/README.md +43 -0
- package/dist/cli.cjs +137 -0
- package/package.json +51 -0
- package/skills/stellar-dev/SKILL.md +146 -0
- package/skills/stellar-dev/advanced-patterns.md +188 -0
- package/skills/stellar-dev/api-rpc-horizon.md +521 -0
- package/skills/stellar-dev/common-pitfalls.md +510 -0
- package/skills/stellar-dev/contracts-soroban.md +565 -0
- package/skills/stellar-dev/ecosystem.md +430 -0
- package/skills/stellar-dev/frontend-stellar-sdk.md +651 -0
- package/skills/stellar-dev/resources.md +306 -0
- package/skills/stellar-dev/security.md +491 -0
- package/skills/stellar-dev/standards-reference.md +94 -0
- package/skills/stellar-dev/stellar-assets.md +419 -0
- package/skills/stellar-dev/testing.md +786 -0
- package/skills/stellar-dev/zk-proofs.md +136 -0
- package/skills/x402-add-paywall/SKILL.md +208 -0
- package/skills/x402-add-paywall/references/patterns.md +132 -0
- package/skills/x402-debug/SKILL.md +92 -0
- package/skills/x402-debug/references/checklist.md +146 -0
- package/skills/x402-explain/SKILL.md +136 -0
- package/skills/x402-init/SKILL.md +129 -0
- package/skills/x402-init/templates/env-example.md +17 -0
- package/skills/x402-init/templates/express/config.ts.md +29 -0
- package/skills/x402-init/templates/express/server.ts.md +30 -0
- package/skills/x402-init/templates/fastify/adapter.ts.md +66 -0
- package/skills/x402-init/templates/fastify/config.ts.md +29 -0
- package/skills/x402-init/templates/fastify/server.ts.md +90 -0
- package/skills/x402-init/templates/hono/config.ts.md +29 -0
- package/skills/x402-init/templates/hono/server.ts.md +31 -0
- package/skills/x402-init/templates/next-app-router/config.ts.md +29 -0
- package/skills/x402-init/templates/next-app-router/server.ts.md +31 -0
- package/skills/x402-stellar/SKILL.md +139 -0
- package/skills/x402-stellar/references/api.md +237 -0
- package/skills/x402-stellar/references/patterns.md +276 -0
- package/skills/x402-stellar/references/setup.md +138 -0
- package/skills/x402-stellar/scripts/check-deps.js +218 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
# Frontend with Stellar SDK (Next.js / React)
|
|
2
|
+
|
|
3
|
+
## Goals
|
|
4
|
+
- Single SDK instance for the app (RPC/Horizon + transaction building)
|
|
5
|
+
- Freighter wallet integration (or multi-wallet via Stellar Wallets Kit)
|
|
6
|
+
- Clean separation of client/server in Next.js
|
|
7
|
+
- Transaction sending with proper confirmation handling
|
|
8
|
+
|
|
9
|
+
## Quick Navigation
|
|
10
|
+
- SDK setup and env config: [SDK Initialization](#sdk-initialization)
|
|
11
|
+
- Wallet integrations: [Wallet Integration](#wallet-integration)
|
|
12
|
+
- Tx build/send patterns: [Transaction Building](#transaction-building), [Transaction Submission](#transaction-submission)
|
|
13
|
+
- React + Next.js patterns: [React Components](#react-components), [Next.js App Router Setup](#nextjs-app-router-setup)
|
|
14
|
+
- Smart wallets/passkeys: [Smart Accounts (Passkey Wallets)](#smart-accounts-passkey-wallets)
|
|
15
|
+
- Production UX checklist: [Transaction UX Checklist](#transaction-ux-checklist)
|
|
16
|
+
|
|
17
|
+
## Recommended Dependencies
|
|
18
|
+
|
|
19
|
+
> **Requires Node.js 20+** — the Stellar SDK dropped Node 18 support.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @stellar/stellar-sdk @stellar/freighter-api
|
|
23
|
+
# Or for multi-wallet support:
|
|
24
|
+
npm install @stellar/stellar-sdk @creit.tech/stellar-wallets-kit
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## SDK Initialization
|
|
28
|
+
|
|
29
|
+
> For the full API reference (RPC methods, Horizon endpoints, migration guide), see [api-rpc-horizon.md](api-rpc-horizon.md).
|
|
30
|
+
|
|
31
|
+
### Basic Setup
|
|
32
|
+
```typescript
|
|
33
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
34
|
+
|
|
35
|
+
// For Testnet
|
|
36
|
+
const testnetServer = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org");
|
|
37
|
+
const testnetRpc = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org");
|
|
38
|
+
const testnetNetworkPassphrase = StellarSdk.Networks.TESTNET;
|
|
39
|
+
|
|
40
|
+
// For Mainnet
|
|
41
|
+
const mainnetServer = new StellarSdk.Horizon.Server("https://horizon.stellar.org");
|
|
42
|
+
const mainnetRpcUrl = process.env.NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL;
|
|
43
|
+
if (!mainnetRpcUrl) throw new Error("Missing NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL");
|
|
44
|
+
const mainnetRpc = new StellarSdk.rpc.Server(mainnetRpcUrl); // set from your chosen RPC provider
|
|
45
|
+
const mainnetNetworkPassphrase = StellarSdk.Networks.PUBLIC;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Environment Configuration
|
|
49
|
+
> Use a provider-specific mainnet RPC URL (see: https://developers.stellar.org/docs/data/apis/rpc/providers).
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// lib/stellar.ts
|
|
53
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
54
|
+
|
|
55
|
+
const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK || "testnet";
|
|
56
|
+
|
|
57
|
+
const requireEnv = (name: string): string => {
|
|
58
|
+
const value = process.env[name];
|
|
59
|
+
if (!value) throw new Error(`Missing required env var: ${name}`);
|
|
60
|
+
return value;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const config = {
|
|
64
|
+
testnet: {
|
|
65
|
+
horizonUrl: "https://horizon-testnet.stellar.org",
|
|
66
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
67
|
+
networkPassphrase: StellarSdk.Networks.TESTNET,
|
|
68
|
+
friendbotUrl: "https://friendbot.stellar.org",
|
|
69
|
+
},
|
|
70
|
+
mainnet: {
|
|
71
|
+
horizonUrl: "https://horizon.stellar.org",
|
|
72
|
+
rpcUrl: requireEnv("NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL"),
|
|
73
|
+
networkPassphrase: StellarSdk.Networks.PUBLIC,
|
|
74
|
+
friendbotUrl: null,
|
|
75
|
+
},
|
|
76
|
+
}[NETWORK]!;
|
|
77
|
+
|
|
78
|
+
export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl);
|
|
79
|
+
export const rpc = new StellarSdk.rpc.Server(config.rpcUrl);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Wallet Integration
|
|
83
|
+
|
|
84
|
+
### Freighter (Primary Browser Wallet)
|
|
85
|
+
```typescript
|
|
86
|
+
// hooks/useFreighter.ts
|
|
87
|
+
import { useState, useEffect, useCallback } from "react";
|
|
88
|
+
import {
|
|
89
|
+
isConnected,
|
|
90
|
+
isAllowed,
|
|
91
|
+
setAllowed,
|
|
92
|
+
getPublicKey,
|
|
93
|
+
signTransaction,
|
|
94
|
+
getNetwork,
|
|
95
|
+
} from "@stellar/freighter-api";
|
|
96
|
+
|
|
97
|
+
export function useFreighter() {
|
|
98
|
+
const [connected, setConnected] = useState(false);
|
|
99
|
+
const [address, setAddress] = useState<string | null>(null);
|
|
100
|
+
const [network, setNetwork] = useState<string | null>(null);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
checkConnection();
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const checkConnection = async () => {
|
|
107
|
+
const freighterConnected = await isConnected();
|
|
108
|
+
if (!freighterConnected) return;
|
|
109
|
+
|
|
110
|
+
const allowed = await isAllowed();
|
|
111
|
+
if (allowed) {
|
|
112
|
+
const pubKey = await getPublicKey();
|
|
113
|
+
const net = await getNetwork();
|
|
114
|
+
setConnected(true);
|
|
115
|
+
setAddress(pubKey);
|
|
116
|
+
setNetwork(net);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const connect = useCallback(async () => {
|
|
121
|
+
const freighterConnected = await isConnected();
|
|
122
|
+
if (!freighterConnected) {
|
|
123
|
+
throw new Error("Freighter extension not installed");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await setAllowed();
|
|
127
|
+
const pubKey = await getPublicKey();
|
|
128
|
+
const net = await getNetwork();
|
|
129
|
+
|
|
130
|
+
setConnected(true);
|
|
131
|
+
setAddress(pubKey);
|
|
132
|
+
setNetwork(net);
|
|
133
|
+
|
|
134
|
+
return pubKey;
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const disconnect = useCallback(() => {
|
|
138
|
+
setConnected(false);
|
|
139
|
+
setAddress(null);
|
|
140
|
+
setNetwork(null);
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
const sign = useCallback(
|
|
144
|
+
async (xdr: string, networkPassphrase: string) => {
|
|
145
|
+
if (!connected) throw new Error("Wallet not connected");
|
|
146
|
+
return signTransaction(xdr, { networkPassphrase });
|
|
147
|
+
},
|
|
148
|
+
[connected]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return { connected, address, network, connect, disconnect, sign };
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Stellar Wallets Kit (Multi-Wallet)
|
|
156
|
+
```typescript
|
|
157
|
+
// hooks/useStellarWallet.ts
|
|
158
|
+
import { useState, useCallback } from "react";
|
|
159
|
+
import {
|
|
160
|
+
StellarWalletsKit,
|
|
161
|
+
WalletNetwork,
|
|
162
|
+
allowAllModules,
|
|
163
|
+
FREIGHTER_ID,
|
|
164
|
+
LOBSTR_ID,
|
|
165
|
+
XBULL_ID,
|
|
166
|
+
} from "@creit.tech/stellar-wallets-kit";
|
|
167
|
+
|
|
168
|
+
const kit = new StellarWalletsKit({
|
|
169
|
+
network: WalletNetwork.TESTNET,
|
|
170
|
+
selectedWalletId: FREIGHTER_ID,
|
|
171
|
+
modules: allowAllModules(),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export function useStellarWallet() {
|
|
175
|
+
const [address, setAddress] = useState<string | null>(null);
|
|
176
|
+
|
|
177
|
+
const connect = useCallback(async () => {
|
|
178
|
+
await kit.openModal({
|
|
179
|
+
onWalletSelected: async (option) => {
|
|
180
|
+
kit.setWallet(option.id);
|
|
181
|
+
const { address } = await kit.getAddress();
|
|
182
|
+
setAddress(address);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
const disconnect = useCallback(() => {
|
|
188
|
+
setAddress(null);
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const sign = useCallback(async (xdr: string) => {
|
|
192
|
+
const { signedTxXdr } = await kit.signTransaction(xdr);
|
|
193
|
+
return signedTxXdr;
|
|
194
|
+
}, []);
|
|
195
|
+
|
|
196
|
+
return { address, connect, disconnect, sign, kit };
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Transaction Building
|
|
201
|
+
|
|
202
|
+
### Basic Payment
|
|
203
|
+
```typescript
|
|
204
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
205
|
+
import { horizon, config } from "@/lib/stellar";
|
|
206
|
+
|
|
207
|
+
export async function buildPaymentTx(
|
|
208
|
+
sourceAddress: string,
|
|
209
|
+
destinationAddress: string,
|
|
210
|
+
amount: string,
|
|
211
|
+
asset: StellarSdk.Asset = StellarSdk.Asset.native()
|
|
212
|
+
) {
|
|
213
|
+
const account = await horizon.loadAccount(sourceAddress);
|
|
214
|
+
|
|
215
|
+
const transaction = new StellarSdk.TransactionBuilder(account, {
|
|
216
|
+
fee: StellarSdk.BASE_FEE,
|
|
217
|
+
networkPassphrase: config.networkPassphrase,
|
|
218
|
+
})
|
|
219
|
+
.addOperation(
|
|
220
|
+
StellarSdk.Operation.payment({
|
|
221
|
+
destination: destinationAddress,
|
|
222
|
+
asset: asset,
|
|
223
|
+
amount: amount,
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
.setTimeout(180)
|
|
227
|
+
.build();
|
|
228
|
+
|
|
229
|
+
return transaction.toXDR();
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Soroban Contract Invocation
|
|
234
|
+
```typescript
|
|
235
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
236
|
+
import { rpc, config } from "@/lib/stellar";
|
|
237
|
+
|
|
238
|
+
export async function invokeContract(
|
|
239
|
+
sourceAddress: string,
|
|
240
|
+
contractId: string,
|
|
241
|
+
method: string,
|
|
242
|
+
args: StellarSdk.xdr.ScVal[]
|
|
243
|
+
) {
|
|
244
|
+
const account = await rpc.getAccount(sourceAddress);
|
|
245
|
+
|
|
246
|
+
const contract = new StellarSdk.Contract(contractId);
|
|
247
|
+
|
|
248
|
+
let transaction = new StellarSdk.TransactionBuilder(account, {
|
|
249
|
+
fee: StellarSdk.BASE_FEE,
|
|
250
|
+
networkPassphrase: config.networkPassphrase,
|
|
251
|
+
})
|
|
252
|
+
.addOperation(contract.call(method, ...args))
|
|
253
|
+
.setTimeout(180)
|
|
254
|
+
.build();
|
|
255
|
+
|
|
256
|
+
// Simulate to get resource estimates
|
|
257
|
+
const simulation = await rpc.simulateTransaction(transaction);
|
|
258
|
+
|
|
259
|
+
if (StellarSdk.rpc.Api.isSimulationError(simulation)) {
|
|
260
|
+
throw new Error(`Simulation failed: ${simulation.error}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Assemble with proper resources
|
|
264
|
+
transaction = StellarSdk.rpc.assembleTransaction(
|
|
265
|
+
transaction,
|
|
266
|
+
simulation
|
|
267
|
+
).build();
|
|
268
|
+
|
|
269
|
+
return transaction.toXDR();
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Building ScVal Arguments
|
|
274
|
+
```typescript
|
|
275
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
276
|
+
|
|
277
|
+
// Common conversions
|
|
278
|
+
const addressVal = StellarSdk.Address.fromString(address).toScVal();
|
|
279
|
+
const i128Val = StellarSdk.nativeToScVal(BigInt(amount), { type: "i128" });
|
|
280
|
+
const u32Val = StellarSdk.nativeToScVal(42, { type: "u32" });
|
|
281
|
+
const stringVal = StellarSdk.nativeToScVal("hello", { type: "string" });
|
|
282
|
+
const symbolVal = StellarSdk.nativeToScVal("transfer", { type: "symbol" });
|
|
283
|
+
|
|
284
|
+
// Struct
|
|
285
|
+
const structVal = StellarSdk.nativeToScVal(
|
|
286
|
+
{ name: "Token", decimals: 7 },
|
|
287
|
+
{
|
|
288
|
+
type: {
|
|
289
|
+
name: ["symbol", null],
|
|
290
|
+
decimals: ["u32", null],
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Vec
|
|
296
|
+
const vecVal = StellarSdk.nativeToScVal([1, 2, 3], { type: "i128" });
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Transaction Submission
|
|
300
|
+
|
|
301
|
+
### Submit and Wait for Confirmation
|
|
302
|
+
```typescript
|
|
303
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
304
|
+
import { rpc, horizon, config } from "@/lib/stellar";
|
|
305
|
+
|
|
306
|
+
export async function submitTransaction(signedXdr: string) {
|
|
307
|
+
const transaction = StellarSdk.TransactionBuilder.fromXDR(
|
|
308
|
+
signedXdr,
|
|
309
|
+
config.networkPassphrase
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// For Soroban transactions, use RPC
|
|
313
|
+
if (transaction.operations.some(op => op.type === "invokeHostFunction")) {
|
|
314
|
+
return submitSorobanTransaction(signedXdr);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// For classic transactions, use Horizon
|
|
318
|
+
return submitClassicTransaction(signedXdr);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function submitSorobanTransaction(signedXdr: string) {
|
|
322
|
+
const transaction = StellarSdk.TransactionBuilder.fromXDR(
|
|
323
|
+
signedXdr,
|
|
324
|
+
config.networkPassphrase
|
|
325
|
+
) as StellarSdk.Transaction;
|
|
326
|
+
|
|
327
|
+
const response = await rpc.sendTransaction(transaction);
|
|
328
|
+
|
|
329
|
+
if (response.status === "ERROR") {
|
|
330
|
+
throw new Error(`Transaction failed: ${response.errorResult}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Poll for completion
|
|
334
|
+
let getResponse = await rpc.getTransaction(response.hash);
|
|
335
|
+
while (getResponse.status === "NOT_FOUND") {
|
|
336
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
337
|
+
getResponse = await rpc.getTransaction(response.hash);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (getResponse.status === "SUCCESS") {
|
|
341
|
+
return {
|
|
342
|
+
hash: response.hash,
|
|
343
|
+
result: getResponse.returnValue,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
throw new Error(`Transaction failed: ${getResponse.status}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function submitClassicTransaction(signedXdr: string) {
|
|
351
|
+
const transaction = StellarSdk.TransactionBuilder.fromXDR(
|
|
352
|
+
signedXdr,
|
|
353
|
+
config.networkPassphrase
|
|
354
|
+
) as StellarSdk.Transaction;
|
|
355
|
+
|
|
356
|
+
const response = await horizon.submitTransaction(transaction);
|
|
357
|
+
return {
|
|
358
|
+
hash: response.hash,
|
|
359
|
+
ledger: response.ledger,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## React Components
|
|
365
|
+
|
|
366
|
+
### Connect Wallet Button
|
|
367
|
+
```tsx
|
|
368
|
+
// components/ConnectButton.tsx
|
|
369
|
+
"use client";
|
|
370
|
+
|
|
371
|
+
import { useFreighter } from "@/hooks/useFreighter";
|
|
372
|
+
|
|
373
|
+
export function ConnectButton() {
|
|
374
|
+
const { connected, address, connect, disconnect } = useFreighter();
|
|
375
|
+
|
|
376
|
+
if (connected && address) {
|
|
377
|
+
return (
|
|
378
|
+
<div className="flex items-center gap-2">
|
|
379
|
+
<span className="text-sm">
|
|
380
|
+
{address.slice(0, 4)}...{address.slice(-4)}
|
|
381
|
+
</span>
|
|
382
|
+
<button
|
|
383
|
+
onClick={disconnect}
|
|
384
|
+
className="px-4 py-2 bg-red-500 text-white rounded"
|
|
385
|
+
>
|
|
386
|
+
Disconnect
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<button
|
|
394
|
+
onClick={connect}
|
|
395
|
+
className="px-4 py-2 bg-blue-500 text-white rounded"
|
|
396
|
+
>
|
|
397
|
+
Connect Wallet
|
|
398
|
+
</button>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Send Payment Form
|
|
404
|
+
```tsx
|
|
405
|
+
// components/SendPayment.tsx
|
|
406
|
+
"use client";
|
|
407
|
+
|
|
408
|
+
import { useState } from "react";
|
|
409
|
+
import { useFreighter } from "@/hooks/useFreighter";
|
|
410
|
+
import { buildPaymentTx, submitTransaction } from "@/lib/transactions";
|
|
411
|
+
|
|
412
|
+
export function SendPayment() {
|
|
413
|
+
const { address, sign } = useFreighter();
|
|
414
|
+
const [destination, setDestination] = useState("");
|
|
415
|
+
const [amount, setAmount] = useState("");
|
|
416
|
+
const [status, setStatus] = useState<string | null>(null);
|
|
417
|
+
const [loading, setLoading] = useState(false);
|
|
418
|
+
|
|
419
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
if (!address) return;
|
|
422
|
+
|
|
423
|
+
setLoading(true);
|
|
424
|
+
setStatus("Building transaction...");
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const xdr = await buildPaymentTx(address, destination, amount);
|
|
428
|
+
|
|
429
|
+
setStatus("Please sign in your wallet...");
|
|
430
|
+
const signedXdr = await sign(xdr, config.networkPassphrase);
|
|
431
|
+
|
|
432
|
+
setStatus("Submitting transaction...");
|
|
433
|
+
const result = await submitTransaction(signedXdr);
|
|
434
|
+
|
|
435
|
+
setStatus(`Success! Hash: ${result.hash}`);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
setStatus(`Error: ${error.message}`);
|
|
438
|
+
} finally {
|
|
439
|
+
setLoading(false);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
445
|
+
<input
|
|
446
|
+
type="text"
|
|
447
|
+
placeholder="Destination Address"
|
|
448
|
+
value={destination}
|
|
449
|
+
onChange={(e) => setDestination(e.target.value)}
|
|
450
|
+
className="w-full p-2 border rounded"
|
|
451
|
+
/>
|
|
452
|
+
<input
|
|
453
|
+
type="text"
|
|
454
|
+
placeholder="Amount (XLM)"
|
|
455
|
+
value={amount}
|
|
456
|
+
onChange={(e) => setAmount(e.target.value)}
|
|
457
|
+
className="w-full p-2 border rounded"
|
|
458
|
+
/>
|
|
459
|
+
<button
|
|
460
|
+
type="submit"
|
|
461
|
+
disabled={loading || !address}
|
|
462
|
+
className="w-full p-2 bg-blue-500 text-white rounded disabled:opacity-50"
|
|
463
|
+
>
|
|
464
|
+
{loading ? "Processing..." : "Send"}
|
|
465
|
+
</button>
|
|
466
|
+
{status && <p className="text-sm">{status}</p>}
|
|
467
|
+
</form>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Next.js App Router Setup
|
|
473
|
+
|
|
474
|
+
### Provider Component
|
|
475
|
+
```tsx
|
|
476
|
+
// app/providers.tsx
|
|
477
|
+
"use client";
|
|
478
|
+
|
|
479
|
+
import { ReactNode } from "react";
|
|
480
|
+
|
|
481
|
+
// Add any context providers here
|
|
482
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
483
|
+
return <>{children}</>;
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Layout
|
|
488
|
+
```tsx
|
|
489
|
+
// app/layout.tsx
|
|
490
|
+
import { Providers } from "./providers";
|
|
491
|
+
|
|
492
|
+
export default function RootLayout({
|
|
493
|
+
children,
|
|
494
|
+
}: {
|
|
495
|
+
children: React.ReactNode;
|
|
496
|
+
}) {
|
|
497
|
+
return (
|
|
498
|
+
<html lang="en">
|
|
499
|
+
<body>
|
|
500
|
+
<Providers>{children}</Providers>
|
|
501
|
+
</body>
|
|
502
|
+
</html>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Data Fetching
|
|
508
|
+
|
|
509
|
+
### Account Balance
|
|
510
|
+
```typescript
|
|
511
|
+
import { horizon } from "@/lib/stellar";
|
|
512
|
+
|
|
513
|
+
export async function getBalance(address: string) {
|
|
514
|
+
try {
|
|
515
|
+
const account = await horizon.loadAccount(address);
|
|
516
|
+
const nativeBalance = account.balances.find(
|
|
517
|
+
(b) => b.asset_type === "native"
|
|
518
|
+
);
|
|
519
|
+
return nativeBalance?.balance || "0";
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (error.response?.status === 404) {
|
|
522
|
+
return "0"; // Account not funded
|
|
523
|
+
}
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Contract State
|
|
530
|
+
```typescript
|
|
531
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
532
|
+
import { rpc } from "@/lib/stellar";
|
|
533
|
+
|
|
534
|
+
export async function getContractData(
|
|
535
|
+
contractId: string,
|
|
536
|
+
key: StellarSdk.xdr.ScVal
|
|
537
|
+
) {
|
|
538
|
+
const ledgerKey = StellarSdk.xdr.LedgerKey.contractData(
|
|
539
|
+
new StellarSdk.xdr.LedgerKeyContractData({
|
|
540
|
+
contract: new StellarSdk.Address(contractId).toScAddress(),
|
|
541
|
+
key: key,
|
|
542
|
+
durability: StellarSdk.xdr.ContractDataDurability.persistent(),
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const entries = await rpc.getLedgerEntries(ledgerKey);
|
|
547
|
+
|
|
548
|
+
if (entries.entries.length === 0) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return StellarSdk.scValToNative(
|
|
553
|
+
entries.entries[0].val.contractData().val()
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Smart Accounts (Passkey Wallets)
|
|
559
|
+
|
|
560
|
+
For passwordless authentication using WebAuthn passkeys, use Smart Account Kit.
|
|
561
|
+
|
|
562
|
+
### Installation
|
|
563
|
+
```bash
|
|
564
|
+
npm install smart-account-kit
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Quick Start
|
|
568
|
+
```typescript
|
|
569
|
+
import { SmartAccountKit, IndexedDBStorage } from 'smart-account-kit';
|
|
570
|
+
|
|
571
|
+
const kit = new SmartAccountKit({
|
|
572
|
+
rpcUrl: 'https://soroban-testnet.stellar.org',
|
|
573
|
+
networkPassphrase: 'Test SDF Network ; September 2015',
|
|
574
|
+
accountWasmHash: 'YOUR_ACCOUNT_WASM_HASH',
|
|
575
|
+
webauthnVerifierAddress: 'CWEBAUTHN_VERIFIER_ADDRESS',
|
|
576
|
+
storage: new IndexedDBStorage(),
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// On page load - silent restore from stored session
|
|
580
|
+
const result = await kit.connectWallet();
|
|
581
|
+
if (!result) {
|
|
582
|
+
showConnectButton(); // No stored session
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Create new wallet with passkey
|
|
586
|
+
const { contractId, credentialId } = await kit.createWallet(
|
|
587
|
+
'My App',
|
|
588
|
+
'user@example.com',
|
|
589
|
+
{ autoSubmit: true }
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// Connect to existing wallet (prompts for passkey)
|
|
593
|
+
await kit.connectWallet({ prompt: true });
|
|
594
|
+
|
|
595
|
+
// Sign and submit transactions
|
|
596
|
+
const result = await kit.signAndSubmit(transaction);
|
|
597
|
+
|
|
598
|
+
// Transfer tokens
|
|
599
|
+
await kit.transfer(tokenContract, recipient, amount);
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Key Features
|
|
603
|
+
- **Session Management**: Automatic credential persistence and silent reconnection
|
|
604
|
+
- **Multiple Signer Types**: Passkeys (secp256r1), Ed25519 keys, policies
|
|
605
|
+
- **Context Rules**: Fine-grained authorization for different operations
|
|
606
|
+
- **Policy Support**: Threshold multisig, spending limits, custom policies
|
|
607
|
+
- **External Wallet Support**: Connect Freighter, LOBSTR via adapters
|
|
608
|
+
- **Gasless Transactions**: Optional relayer integration for fee sponsoring
|
|
609
|
+
|
|
610
|
+
### Fee Sponsorship with OpenZeppelin Relayer
|
|
611
|
+
|
|
612
|
+
The [OpenZeppelin Relayer](https://docs.openzeppelin.com/relayer/stellar) (also called Stellar Channels Service) handles gasless transaction submission. It replaces the deprecated Launchtube service and uses Stellar's native fee bump mechanism so users don't need XLM for fees.
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
import * as RPChannels from "@openzeppelin/relayer-plugin-channels";
|
|
616
|
+
|
|
617
|
+
const client = new RPChannels.ChannelsClient({
|
|
618
|
+
baseUrl: "https://channels.openzeppelin.com/testnet",
|
|
619
|
+
apiKey: "your-api-key",
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Submit a Soroban contract call with fee sponsorship
|
|
623
|
+
const response = await client.submitSorobanTransaction({
|
|
624
|
+
func: contractFunc,
|
|
625
|
+
auth: contractAuth,
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
- **Testnet hosted instance**: `https://channels.openzeppelin.com/testnet` (API keys at `/gen`)
|
|
630
|
+
- **Production**: Self-host via Docker ([GitHub](https://github.com/OpenZeppelin/openzeppelin-relayer))
|
|
631
|
+
- **Stellar docs**: https://developers.stellar.org/docs/tools/openzeppelin-relayer
|
|
632
|
+
|
|
633
|
+
### Resources
|
|
634
|
+
- **GitHub**: https://github.com/kalepail/smart-account-kit
|
|
635
|
+
- **OpenZeppelin Contracts**: https://github.com/OpenZeppelin/stellar-contracts
|
|
636
|
+
- **Legacy SDK**: https://github.com/kalepail/passkey-kit (for simpler use cases)
|
|
637
|
+
|
|
638
|
+
## Transaction UX Checklist
|
|
639
|
+
|
|
640
|
+
- [ ] Show loading state during wallet signing
|
|
641
|
+
- [ ] Display transaction hash immediately after submission
|
|
642
|
+
- [ ] Track confirmation status (pending → success/failed)
|
|
643
|
+
- [ ] Handle common errors with clear messages:
|
|
644
|
+
- Wallet not connected
|
|
645
|
+
- User rejected signing
|
|
646
|
+
- Insufficient XLM for fees
|
|
647
|
+
- Account not funded
|
|
648
|
+
- Network mismatch (wallet on wrong network)
|
|
649
|
+
- Transaction timeout/expired
|
|
650
|
+
- [ ] Prevent double-submission while processing
|
|
651
|
+
- [ ] Show destination and amount before signing
|