forked from SiLioLabs/PayFlow
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtestnet-setup.ts
More file actions
196 lines (165 loc) · 7.46 KB
/
Copy pathtestnet-setup.ts
File metadata and controls
196 lines (165 loc) · 7.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/**
* testnet-setup.ts — Create a reproducible set of funded test identities for
* integration and E2E testing against a testnet FlowPay deployment.
*
* Given a numeric --seed, deterministically derives keypairs (the same seed
* always produces the same accounts), funds any that aren't yet funded via
* Friendbot, and writes a manifest describing them. Re-running with the same
* --seed reuses the same identities instead of minting new ones, so test
* runs (and CI) get a stable, reproducible cast of accounts.
*
* Usage:
* npx tsx scripts/testnet-setup.ts --seed 1 --users 3 --merchants 1
*
* Environment variables:
* RPC_URL — Soroban RPC endpoint, used only to check account existence
* (default: https://soroban-testnet.stellar.org)
* FRIENDBOT_URL — Friendbot funding endpoint (default: https://friendbot.stellar.org)
*
* Output:
* Writes scripts/.testnet-manifest.<seed>.json containing each identity's
* public key, secret key, and role (user/merchant). This file is test
* fixture data for a throwaway testnet account, not a secret worth
* protecting — do not reuse these keys for anything beyond testnet testing.
*
* This script only creates and funds accounts — it does not call `subscribe`
* or any other contract function. Use the Soroban CLI against the generated
* identities (see docs/TESTING.md's "Integration Testing" section) once they
* exist and are funded.
*
* Exit codes:
* 0 — all requested identities exist and are funded
* 1 — invalid arguments or a funding request failed
*/
import { createHash } from "node:crypto";
import { writeFileSync, existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { Keypair } from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";
// ── Configuration ────────────────────────────────────────────────────────────
const RPC_URL = process.env.RPC_URL || "https://soroban-testnet.stellar.org";
const FRIENDBOT_URL = process.env.FRIENDBOT_URL || "https://friendbot.stellar.org";
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
// ── Argument parsing ─────────────────────────────────────────────────────────
interface SetupArgs {
seed: number;
users: number;
merchants: number;
}
function parseArgs(argv: string[]): SetupArgs {
let seed = 1;
let users = 3;
let merchants = 1;
for (let i = 2; i < argv.length; i++) {
switch (argv[i]) {
case "--seed":
seed = parseInt(argv[++i], 10);
break;
case "--users":
users = parseInt(argv[++i], 10);
break;
case "--merchants":
merchants = parseInt(argv[++i], 10);
break;
default:
console.error(`Unknown argument: ${argv[i]}`);
console.error("Usage: testnet-setup.ts --seed <n> --users <n> --merchants <n>");
process.exit(1);
}
}
if (!Number.isInteger(seed) || !Number.isInteger(users) || !Number.isInteger(merchants)) {
console.error("ERROR: --seed, --users, and --merchants must be integers.");
process.exit(1);
}
if (users < 1 || merchants < 1) {
console.error("ERROR: --users and --merchants must each be at least 1.");
process.exit(1);
}
return { seed, users, merchants };
}
// ── Deterministic identity derivation ────────────────────────────────────────
interface Identity {
role: "user" | "merchant";
index: number;
publicKey: string;
secretKey: string;
}
/**
* Derives a stable ed25519 keypair from (seed, role, index) so the same
* --seed always reproduces the same set of testnet identities.
*/
function deriveKeypair(seed: number, role: "user" | "merchant", index: number): Keypair {
const hash = createHash("sha256").update(`payflow-testnet-setup:${seed}:${role}:${index}`).digest();
return Keypair.fromRawEd25519Seed(hash);
}
function manifestPath(seed: number): string {
return join(SCRIPT_DIR, `.testnet-manifest.${seed}.json`);
}
function loadOrCreateManifest(args: SetupArgs): Identity[] {
const path = manifestPath(args.seed);
if (existsSync(path)) {
const existing: Identity[] = JSON.parse(readFileSync(path, "utf-8"));
const haveUsers = existing.filter((i) => i.role === "user").length;
const haveMerchants = existing.filter((i) => i.role === "merchant").length;
if (haveUsers >= args.users && haveMerchants >= args.merchants) {
console.log(`Reusing existing manifest: ${path}`);
return existing;
}
}
const identities: Identity[] = [];
for (let i = 0; i < args.users; i++) {
const kp = deriveKeypair(args.seed, "user", i);
identities.push({ role: "user", index: i, publicKey: kp.publicKey(), secretKey: kp.secret() });
}
for (let i = 0; i < args.merchants; i++) {
const kp = deriveKeypair(args.seed, "merchant", i);
identities.push({ role: "merchant", index: i, publicKey: kp.publicKey(), secretKey: kp.secret() });
}
writeFileSync(path, JSON.stringify(identities, null, 2));
console.log(`Wrote manifest: ${path}`);
return identities;
}
// ── Funding ───────────────────────────────────────────────────────────────────
async function isFunded(server: Server, publicKey: string): Promise<boolean> {
try {
await server.getAccount(publicKey);
return true;
} catch {
return false;
}
}
async function fundViaFriendbot(publicKey: string): Promise<void> {
const response = await fetch(`${FRIENDBOT_URL}?addr=${encodeURIComponent(publicKey)}`);
if (!response.ok && response.status !== 400) {
// Friendbot returns 400 if the account is already funded — treat that as success.
throw new Error(`Friendbot funding failed for ${publicKey}: HTTP ${response.status}`);
}
}
// ── Main ─────────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
const args = parseArgs(process.argv);
const server = new Server(RPC_URL);
console.log(`Setting up testnet fixtures: seed=${args.seed} users=${args.users} merchants=${args.merchants}`);
console.log("");
const identities = loadOrCreateManifest(args);
for (const identity of identities) {
const label = `${identity.role}[${identity.index}]`;
const funded = await isFunded(server, identity.publicKey);
if (funded) {
console.log(` ${label} ${identity.publicKey} — already funded`);
continue;
}
console.log(` ${label} ${identity.publicKey} — funding via Friendbot...`);
await fundViaFriendbot(identity.publicKey);
console.log(` ${label} ${identity.publicKey} — funded`);
}
console.log("");
console.log(`Manifest: ${manifestPath(args.seed)}`);
console.log("Next step: use the Soroban CLI with these identities to call subscribe()/charge()");
console.log("against your deployed contract — see docs/TESTING.md, Integration Testing section.");
}
main().catch((err) => {
console.error("testnet-setup failed:", err instanceof Error ? err.message : err);
process.exit(1);
});