@algopayoracle/oracle-sdk 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ /**
2
+ * @algopayoracle/oracle-sdk — Custom Payment Adapter Example
3
+ *
4
+ * Demonstrates how to integrate any payment gateway with AlgoPay Oracle.
5
+ * The SDK's core (AlgoPayClient) only speaks PaymentEvent.
6
+ * Your adapter is the translation layer between your gateway and PaymentEvent.
7
+ *
8
+ * This file shows three adapters:
9
+ * 1. PayUAdapter — real-world Indian payment gateway
10
+ * 2. PhonePeAdapter — UPI-native gateway
11
+ * 3. GenericAdapter — minimal template to copy for any provider
12
+ *
13
+ * The PaymentAdapter interface (from index.d.ts):
14
+ *
15
+ * interface PaymentAdapter {
16
+ * parseWebhook(rawBody: Buffer | string, signature: string): PaymentEvent | null;
17
+ * }
18
+ *
19
+ * Rules for a correct adapter:
20
+ * 1. Verify the gateway's own signature/checksum before trusting any field
21
+ * 2. Return null (never throw) on invalid signature or non-payment events
22
+ * 3. Amount must come from the gateway payload — never from the request body directly
23
+ * 4. Set provider so canonical_id = "provider:payment_id" is unique per rail
24
+ */
25
+
26
+ "use strict";
27
+
28
+ const crypto = require("crypto");
29
+ const { AlgoPayClient } = require("../src");
30
+
31
+ // ════════════════════════════════════════════════════════════════════════════
32
+ // 1. PayUAdapter — PayU India
33
+ // Docs: https://devguide.payu.in/webhook/webhook-overview/
34
+ // ════════════════════════════════════════════════════════════════════════════
35
+
36
+ class PayUAdapter {
37
+ /**
38
+ * @param {object} opts
39
+ * @param {string} opts.merchantSalt - PayU merchant salt
40
+ * @param {string} [opts.defaultAction]
41
+ */
42
+ constructor({ merchantSalt, defaultAction = "unlock" }) {
43
+ if (!merchantSalt) throw new Error("PayUAdapter: merchantSalt is required");
44
+ this.merchantSalt = merchantSalt;
45
+ this.defaultAction = defaultAction;
46
+ }
47
+
48
+ /**
49
+ * PayU sends a hash in the payload for webhook verification.
50
+ * Hash formula (reverse): sha512(salt|status||||||udf5|udf4|udf3|udf2|udf1|email|productinfo|amount|txnid|key)
51
+ */
52
+ parseWebhook(rawBody, _signature) {
53
+ let body;
54
+ try { body = Object.fromEntries(new URLSearchParams(rawBody.toString())); }
55
+ catch { return null; }
56
+
57
+ if (body.status !== "success") return null;
58
+ if (!body.txnid || !body.amount) return null;
59
+
60
+ // Verify PayU hash (simplified — see PayU docs for full field list)
61
+ const hashString = [
62
+ this.merchantSalt,
63
+ body.status,
64
+ "", "", "", "", // udf5–udf2 (empty if not used)
65
+ body.udf1 || "",
66
+ body.email || "",
67
+ body.productinfo || "",
68
+ body.amount,
69
+ body.txnid,
70
+ body.key,
71
+ ].join("|");
72
+
73
+ const expectedHash = crypto.createHash("sha512").update(hashString).digest("hex");
74
+ if (body.hash !== expectedHash) return null;
75
+
76
+ return {
77
+ payment_id: body.txnid,
78
+ amount: Math.round(Number(body.amount)), // PayU sends as string "100.00"
79
+ currency: "INR",
80
+ action: this.defaultAction,
81
+ provider: "payu",
82
+ };
83
+ }
84
+ }
85
+
86
+ // ════════════════════════════════════════════════════════════════════════════
87
+ // 2. PhonePeAdapter — PhonePe Business
88
+ // Docs: https://developer.phonepe.com/v1/reference/check-status-api
89
+ // ════════════════════════════════════════════════════════════════════════════
90
+
91
+ class PhonePeAdapter {
92
+ /**
93
+ * @param {object} opts
94
+ * @param {string} opts.saltKey - PhonePe salt key
95
+ * @param {number} opts.saltIndex - PhonePe salt index (usually 1)
96
+ * @param {string} [opts.defaultAction]
97
+ */
98
+ constructor({ saltKey, saltIndex = 1, defaultAction = "unlock" }) {
99
+ if (!saltKey) throw new Error("PhonePeAdapter: saltKey is required");
100
+ this.saltKey = saltKey;
101
+ this.saltIndex = saltIndex;
102
+ this.defaultAction = defaultAction;
103
+ }
104
+
105
+ /**
106
+ * PhonePe sends X-VERIFY header: sha256(base64payload + "/pg/v1/pay" + salt) + "###" + saltIndex
107
+ */
108
+ parseWebhook(rawBody, xVerifyHeader) {
109
+ if (!xVerifyHeader) return null;
110
+
111
+ const [receivedHash, receivedIndex] = xVerifyHeader.split("###");
112
+ if (Number(receivedIndex) !== this.saltIndex) return null;
113
+
114
+ const base64Body = Buffer.from(rawBody).toString("base64");
115
+ const hashInput = base64Body + "/pg/v1/pay" + this.saltKey;
116
+ const expectedHash = crypto.createHash("sha256").update(hashInput).digest("hex");
117
+
118
+ if (receivedHash !== expectedHash) return null;
119
+
120
+ let parsed;
121
+ try { parsed = JSON.parse(Buffer.from(base64Body, "base64").toString()); }
122
+ catch { return null; }
123
+
124
+ const data = parsed?.data;
125
+ if (!data?.merchantTransactionId || !data?.amount) return null;
126
+ if (parsed?.code !== "PAYMENT_SUCCESS") return null;
127
+
128
+ return {
129
+ payment_id: data.merchantTransactionId,
130
+ amount: Math.round(data.amount / 100), // PhonePe sends in paise
131
+ currency: "INR",
132
+ action: this.defaultAction,
133
+ provider: "phonepe",
134
+ };
135
+ }
136
+ }
137
+
138
+ // ════════════════════════════════════════════════════════════════════════════
139
+ // 3. GenericAdapter — minimal copy-paste template
140
+ //
141
+ // Steps to adapt for any gateway:
142
+ // a) Replace verifySignature() with your gateway's HMAC/checksum method
143
+ // b) Map gateway fields → payment_id, amount, currency
144
+ // c) Set provider to a unique lowercase label
145
+ // ════════════════════════════════════════════════════════════════════════════
146
+
147
+ class GenericAdapter {
148
+ constructor({ secret, defaultAction = "unlock" }) {
149
+ this.secret = secret;
150
+ this.defaultAction = defaultAction;
151
+ }
152
+
153
+ parseWebhook(rawBody, signature) {
154
+ // a) Verify signature — replace this with your gateway's method
155
+ if (!this._verifySignature(rawBody, signature)) return null;
156
+
157
+ let body;
158
+ try { body = JSON.parse(rawBody.toString()); }
159
+ catch { return null; }
160
+
161
+ // b) Only process payment success events
162
+ if (body.event_type !== "payment.success") return null;
163
+
164
+ // c) Map to PaymentEvent
165
+ return {
166
+ payment_id: body.transaction_id, // gateway's unique ID
167
+ amount: Math.round(Number(body.amount)), // always integer
168
+ currency: (body.currency || "INR").toUpperCase(),
169
+ action: this.defaultAction,
170
+ provider: "mygateway", // your unique label
171
+ };
172
+ }
173
+
174
+ _verifySignature(rawBody, signature) {
175
+ // Replace with your gateway's actual verification
176
+ const expected = crypto
177
+ .createHmac("sha256", this.secret)
178
+ .update(rawBody)
179
+ .digest("hex");
180
+ const received = Buffer.from(signature || "", "hex");
181
+ if (expected.length !== received.length) return false;
182
+ return crypto.timingSafeEqual(Buffer.from(expected, "hex"), received);
183
+ }
184
+ }
185
+
186
+ // ════════════════════════════════════════════════════════════════════════════
187
+ // Wiring into Express (same pattern for all adapters)
188
+ // ════════════════════════════════════════════════════════════════════════════
189
+
190
+ function wireAdapter(app, path, adapter, client, limiter) {
191
+ app.post(path, limiter, async (req, res) => {
192
+ // Every adapter implements the same parseWebhook interface
193
+ const event = adapter.parseWebhook(
194
+ req.rawBody,
195
+ req.headers["x-signature"] || req.headers["x-verify"] || req.headers["x-razorpay-signature"]
196
+ );
197
+
198
+ if (!event) return res.status(401).json({ error: "invalid webhook signature" });
199
+
200
+ try {
201
+ const result = await client.verifyAndCommit(event);
202
+ res.json({ received: true, txId: result.txId });
203
+ } catch (e) {
204
+ res.status(500).json({ error: e.message });
205
+ }
206
+ });
207
+ }
208
+
209
+ // ────────────────────────────────────────────────────────────────────────────
210
+ // Demo: all three adapters wired to the same client
211
+ // ────────────────────────────────────────────────────────────────────────────
212
+
213
+ if (require.main === module) {
214
+ require("dotenv").config();
215
+
216
+ const express = require("express");
217
+ const app = express();
218
+ const client = new AlgoPayClient({
219
+ mnemonic: process.env.ORACLE_MNEMONIC,
220
+ network: process.env.ALGO_NETWORK || "testnet",
221
+ appId: process.env.ALGO_APP_ID ? Number(process.env.ALGO_APP_ID) : null,
222
+ });
223
+
224
+ // Raw body middleware
225
+ app.use((req, _res, next) => {
226
+ const chunks = [];
227
+ req.on("data", c => chunks.push(c));
228
+ req.on("end", () => {
229
+ req.rawBody = Buffer.concat(chunks);
230
+ try { req.body = JSON.parse(req.rawBody); } catch { req.body = {}; }
231
+ next();
232
+ });
233
+ });
234
+
235
+ const noopLimiter = (_req, _res, next) => next(); // replace with express-rate-limit in production
236
+
237
+ if (process.env.PAYU_SALT) {
238
+ const payu = new PayUAdapter({ merchantSalt: process.env.PAYU_SALT });
239
+ wireAdapter(app, "/webhook/payu", payu, client, noopLimiter);
240
+ console.log("PayU adapter wired → POST /webhook/payu");
241
+ }
242
+
243
+ if (process.env.PHONEPE_SALT) {
244
+ const phonepe = new PhonePeAdapter({ saltKey: process.env.PHONEPE_SALT });
245
+ wireAdapter(app, "/webhook/phonepe", phonepe, client, noopLimiter);
246
+ console.log("PhonePe adapter wired → POST /webhook/phonepe");
247
+ }
248
+
249
+ app.listen(5000, () => console.log("Custom adapter demo → http://localhost:5000"));
250
+ }
251
+
252
+ module.exports = { PayUAdapter, PhonePeAdapter, GenericAdapter };
@@ -0,0 +1,390 @@
1
+ /**
2
+ * @algopayoracle/oracle-sdk — Production Express Server Example
3
+ *
4
+ * This is the reference implementation for integrating the SDK into a
5
+ * production Express backend. Copy and adapt to your project.
6
+ *
7
+ * TWO SERVERS:
8
+ * Public (PORT, 0.0.0.0) — webhooks, payment verify, proof lookup
9
+ * Admin (ADMIN_PORT, 127.0.0.1) — oracle rotation, stats
10
+ *
11
+ * Run:
12
+ * node examples/express-webhook.js
13
+ */
14
+
15
+ "use strict";
16
+ require("dotenv").config();
17
+
18
+ const express = require("express");
19
+ const cors = require("cors");
20
+ const rateLimit = require("express-rate-limit");
21
+ const crypto = require("crypto");
22
+
23
+ const {
24
+ AlgoPayClient,
25
+ RazorpayAdapter,
26
+ createLogger,
27
+ createOrderStore,
28
+ } = require("../src");
29
+
30
+ // ─── Environment validation ───────────────────────────────────────────────────
31
+
32
+ const IS_PRODUCTION = process.env.NODE_ENV === "production";
33
+ const DEMO_MODE = process.env.DEMO_MODE === "true" && !IS_PRODUCTION;
34
+
35
+ const log = createLogger("server");
36
+
37
+ function requireEnv(name) {
38
+ const val = process.env[name];
39
+ if (!val) { log.error(`${name} is required but not set`); process.exit(1); }
40
+ return val;
41
+ }
42
+
43
+ requireEnv("ORACLE_MNEMONIC");
44
+ if (IS_PRODUCTION) {
45
+ requireEnv("ADMIN_API_KEY");
46
+ requireEnv("ALLOWED_ORIGINS");
47
+ } else {
48
+ if (!process.env.ADMIN_API_KEY) log.warn("ADMIN_API_KEY not set — admin server unprotected");
49
+ if (!process.env.ALGO_APP_ID) log.warn("ALGO_APP_ID not set — running in anchor mode");
50
+ }
51
+
52
+ // ─── Oracle client ────────────────────────────────────────────────────────────
53
+
54
+ const client = new AlgoPayClient({
55
+ mnemonic: process.env.ORACLE_MNEMONIC,
56
+ network: process.env.ALGO_NETWORK || "testnet",
57
+ appId: process.env.ALGO_APP_ID ? Number(process.env.ALGO_APP_ID) : null,
58
+ });
59
+
60
+ log.info("Oracle initialised", {
61
+ address: client.getAddress(),
62
+ network: process.env.ALGO_NETWORK || "testnet",
63
+ app_id: client.appId ?? "anchor mode",
64
+ });
65
+
66
+ // ─── Order store ──────────────────────────────────────────────────────────────
67
+ // In-memory by default. Set REDIS_URL to use Redis (see store.js for interface).
68
+
69
+ const orderStore = createOrderStore();
70
+
71
+ // ─── Razorpay adapter (optional) ──────────────────────────────────────────────
72
+
73
+ const razorpay = (process.env.RAZORPAY_KEY_ID && process.env.RAZORPAY_KEY_SECRET)
74
+ ? new RazorpayAdapter({
75
+ keyId: process.env.RAZORPAY_KEY_ID,
76
+ keySecret: process.env.RAZORPAY_KEY_SECRET,
77
+ orderStore, // shared so createOrder amounts are enforced in parseClientPayment
78
+ })
79
+ : null;
80
+
81
+ // ─── Shared middleware ─────────────────────────────────────────────────────────
82
+
83
+ const MAX_BODY = 512 * 1024;
84
+
85
+ function rawBody(req, res, next) {
86
+ const chunks = [];
87
+ let size = 0;
88
+ req.on("data", chunk => {
89
+ size += chunk.length;
90
+ if (size > MAX_BODY) { req.destroy(); return res.status(413).json({ error: "body too large" }); }
91
+ chunks.push(chunk);
92
+ });
93
+ req.on("end", () => {
94
+ req.rawBody = Buffer.concat(chunks);
95
+ try { req.body = JSON.parse(req.rawBody.toString()); } catch { req.body = {}; }
96
+ next();
97
+ });
98
+ }
99
+
100
+ function requestLogger(req, res, next) {
101
+ req.requestId = crypto.randomUUID();
102
+ req.log = log.child({ requestId: req.requestId });
103
+ const start = Date.now();
104
+ res.on("finish", () => {
105
+ const level = res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warn" : "info";
106
+ req.log[level](`${req.method} ${req.path}`, { status: res.statusCode, ms: Date.now() - start });
107
+ });
108
+ next();
109
+ }
110
+
111
+ function requireAdminKey(req, res, next) {
112
+ const key = process.env.ADMIN_API_KEY;
113
+ if (!key) return res.status(503).json({ error: "ADMIN_API_KEY not configured" });
114
+ if (req.headers["x-admin-key"] !== key) return res.status(401).json({ error: "unauthorized" });
115
+ next();
116
+ }
117
+
118
+ function sendError(res, status, message, err, reqLog) {
119
+ const logger = reqLog || log;
120
+ if (err) logger.error(message, { error: err.message });
121
+ res.status(status).json({ error: IS_PRODUCTION ? message : (err?.message || message) });
122
+ }
123
+
124
+ // ─── Rate limiters ────────────────────────────────────────────────────────────
125
+
126
+ const limiterDefault = rateLimit({
127
+ windowMs: 60 * 1000, max: 60,
128
+ standardHeaders: true, legacyHeaders: false,
129
+ message: { error: "too many requests" },
130
+ });
131
+
132
+ const limiterPayment = rateLimit({
133
+ windowMs: 60 * 1000, max: 10, // each verify-payment triggers an on-chain tx
134
+ standardHeaders: true, legacyHeaders: false,
135
+ message: { error: "too many payment requests" },
136
+ });
137
+
138
+ const limiterWebhook = rateLimit({
139
+ windowMs: 60 * 1000, max: 120, // higher — legitimate burst from provider is possible
140
+ standardHeaders: true, legacyHeaders: false,
141
+ message: { error: "too many requests" },
142
+ });
143
+
144
+ // ════════════════════════════════════════════════════════════════════════════
145
+ // PUBLIC SERVER
146
+ // ════════════════════════════════════════════════════════════════════════════
147
+
148
+ const publicApp = express();
149
+
150
+ const allowedOrigins = process.env.ALLOWED_ORIGINS
151
+ ? process.env.ALLOWED_ORIGINS.split(",").map(s => s.trim())
152
+ : ["http://localhost:5173", "http://localhost:3000"];
153
+
154
+ publicApp.use(cors({ origin: allowedOrigins, methods: ["GET", "POST"] }));
155
+ publicApp.use(requestLogger);
156
+ publicApp.use(rawBody);
157
+
158
+ publicApp.get("/health", limiterDefault, async (_req, res) => {
159
+ try {
160
+ const status = await client.algod.status().do();
161
+ res.json({ ok: true, network: client.network, round: Number(status["last-round"]), demo_mode: DEMO_MODE });
162
+ } catch (e) {
163
+ res.status(503).json({ ok: false, error: IS_PRODUCTION ? "algod unreachable" : e.message });
164
+ }
165
+ });
166
+
167
+ publicApp.get("/oracle/info", limiterDefault, (_req, res) => {
168
+ res.json({ address: client.getAddress(), pubkey_base64: client.getPublicKeyBase64(), network: client.network, app_id: client.appId });
169
+ });
170
+
171
+ // POST /create-order
172
+ // Step 1 of payment flow — get an order ID before opening checkout UI.
173
+ // Amount is stored server-side here and enforced in /verify-payment.
174
+ publicApp.post("/create-order", limiterPayment, async (req, res) => {
175
+ const amount = Math.round(Number(req.body.amount || 100));
176
+ const currency = (req.body.currency || "INR").toUpperCase();
177
+
178
+ if (!Number.isInteger(amount) || amount <= 0) {
179
+ return res.status(400).json({ error: "amount must be a positive integer" });
180
+ }
181
+
182
+ if (razorpay) {
183
+ try {
184
+ // createOrder writes to orderStore internally via the shared reference
185
+ const order = await razorpay.createOrder({ amount, currency });
186
+ req.log.info("order created", { order_id: order.order_id, amount, provider: "razorpay" });
187
+ return res.json({ provider: "razorpay", ...order });
188
+ } catch (e) {
189
+ return sendError(res, 502, "order creation failed", e, req.log);
190
+ }
191
+ }
192
+
193
+ if (DEMO_MODE) {
194
+ const order_id = `demo_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
195
+ await orderStore.set(order_id, { amount, currency });
196
+ req.log.info("demo order created", { order_id, amount });
197
+ return res.json({ provider: "demo", order_id, amount, currency, key_id: null });
198
+ }
199
+
200
+ res.status(503).json({
201
+ error: "no payment provider configured",
202
+ hint: IS_PRODUCTION ? "configure RAZORPAY_KEY_ID/SECRET" : "set DEMO_MODE=true for local dev",
203
+ });
204
+ });
205
+
206
+ // POST /webhook/razorpay — server-to-server from Razorpay on payment.captured
207
+ publicApp.post("/webhook/razorpay", limiterWebhook, async (req, res) => {
208
+ if (!razorpay) return res.status(503).json({ error: "Razorpay not configured" });
209
+
210
+ const event = razorpay.parseWebhook(req.rawBody, req.headers["x-razorpay-signature"]);
211
+ if (!event) {
212
+ req.log.warn("webhook rejected — invalid signature");
213
+ return res.status(401).json({ error: "invalid signature" });
214
+ }
215
+
216
+ req.log.info("webhook received", { payment_id: event.payment_id, amount: event.amount });
217
+
218
+ try {
219
+ const result = await client.verifyAndCommit(event);
220
+ req.log.info("webhook committed", { payment_id: event.payment_id, txId: result.txId });
221
+ res.json({ received: true, txId: result.txId });
222
+ } catch (e) {
223
+ sendError(res, 500, "oracle submission failed", e, req.log);
224
+ }
225
+ });
226
+
227
+ // POST /verify-payment — frontend calls this after checkout success
228
+ // Amount is ALWAYS resolved from orderStore — never from request body.
229
+ publicApp.post("/verify-payment", limiterPayment, async (req, res) => {
230
+ const { razorpay_order_id, razorpay_payment_id, razorpay_signature, order_id, action = "unlock" } = req.body;
231
+ const isRazorpay = razorpay_payment_id && razorpay_signature;
232
+ let event;
233
+
234
+ if (isRazorpay) {
235
+ if (!razorpay) return res.status(503).json({ success: false, error: "Razorpay not configured" });
236
+ try {
237
+ // parseClientPayment: verifies HMAC, resolves amount from orderStore (never client body)
238
+ event = await razorpay.parseClientPayment({ razorpay_order_id, razorpay_payment_id, razorpay_signature, action });
239
+ } catch (e) {
240
+ req.log.warn("client payment rejected", { error: e.message });
241
+ return res.status(401).json({ success: false, error: e.message });
242
+ }
243
+ req.log.info("Razorpay payment verified", { payment_id: event.payment_id, amount: event.amount });
244
+
245
+ } else {
246
+ if (!DEMO_MODE) {
247
+ return res.status(503).json({
248
+ success: false,
249
+ error: IS_PRODUCTION ? "demo mode disabled in production" : "set DEMO_MODE=true",
250
+ });
251
+ }
252
+ const effectiveId = razorpay_order_id || order_id;
253
+ const record = await orderStore.consume(effectiveId);
254
+ if (!record) return res.status(400).json({ success: false, error: "order not found or expired — call /create-order first" });
255
+
256
+ await new Promise(r => setTimeout(r, 600));
257
+ event = { payment_id: `demo_${Date.now()}`, amount: record.amount, currency: record.currency, action, provider: "demo" };
258
+ req.log.info("demo payment", { amount: event.amount, action });
259
+ }
260
+
261
+ try {
262
+ const result = await client.verifyAndCommit(event);
263
+ req.log.info("payment committed on-chain", { payment_id: event.payment_id, txId: result.txId });
264
+ res.json({ success: true, ...result });
265
+ } catch (e) {
266
+ sendError(res, 500, "oracle submission failed", e, req.log);
267
+ }
268
+ });
269
+
270
+ // GET /verify-proof/:txId — frontend polls this to confirm on-chain proof
271
+ publicApp.get("/verify-proof/:txId", limiterDefault, async (req, res) => {
272
+ try {
273
+ const result = await client.verifyProof(req.params.txId);
274
+ res.json(result);
275
+ } catch (e) {
276
+ sendError(res, 500, "verification failed", e, req.log);
277
+ }
278
+ });
279
+
280
+ // ─── Generic gateway (any provider pattern — documented, not wired) ───────────
281
+ //
282
+ // publicApp.post("/webhook/payu", limiterWebhook, async (req, res) => {
283
+ // if (!verifyPayUChecksum(req.rawBody, req.headers["x-payu-checksum"])) {
284
+ // return res.status(401).end();
285
+ // }
286
+ // const result = await client.verifyAndCommit({
287
+ // payment_id: req.body.mihpayid,
288
+ // amount: Math.round(Number(req.body.amount)),
289
+ // currency: "INR",
290
+ // action: "unlock",
291
+ // provider: "payu",
292
+ // });
293
+ // res.json({ received: true, txId: result.txId });
294
+ // });
295
+
296
+ // ════════════════════════════════════════════════════════════════════════════
297
+ // ADMIN SERVER (127.0.0.1 only)
298
+ // ════════════════════════════════════════════════════════════════════════════
299
+
300
+ const adminApp = express();
301
+ const adminLog = log.child({ server: "admin" });
302
+
303
+ adminApp.use(rawBody);
304
+ adminApp.use(requireAdminKey);
305
+
306
+ adminApp.get("/status", async (_req, res) => {
307
+ const info = {
308
+ address: client.getAddress(),
309
+ network: client.network,
310
+ app_id: client.appId,
311
+ demo_mode: DEMO_MODE,
312
+ node_env: process.env.NODE_ENV || "development",
313
+ order_store: { type: "in-memory", size: orderStore.size?.() ?? "unknown" },
314
+ };
315
+ if (client.appId) {
316
+ try {
317
+ info.total_verified = await client.getTotalVerified();
318
+ info.oracle_count = await client.getOracleCount();
319
+ } catch (e) {
320
+ info.contract_error = IS_PRODUCTION ? "unreachable" : e.message;
321
+ }
322
+ }
323
+ res.json(info);
324
+ });
325
+
326
+ adminApp.post("/oracle/add", async (req, res) => {
327
+ const { address } = req.body;
328
+ if (!address) return res.status(400).json({ error: "address required" });
329
+ try {
330
+ const txId = await client.addOracle(address);
331
+ adminLog.info("oracle added", { address, txId });
332
+ res.json({ success: true, txId, added: address });
333
+ } catch (e) {
334
+ adminLog.error("addOracle failed", { address, error: e.message });
335
+ res.status(500).json({ success: false, error: e.message });
336
+ }
337
+ });
338
+
339
+ adminApp.post("/oracle/remove", async (req, res) => {
340
+ const { address } = req.body;
341
+ if (!address) return res.status(400).json({ error: "address required" });
342
+ try {
343
+ const txId = await client.removeOracle(address);
344
+ adminLog.info("oracle removed", { address, txId });
345
+ res.json({ success: true, txId, removed: address });
346
+ } catch (e) {
347
+ adminLog.error("removeOracle failed", { address, error: e.message });
348
+ res.status(500).json({ success: false, error: e.message });
349
+ }
350
+ });
351
+
352
+ adminApp.get("/oracle/check", async (req, res) => {
353
+ const { address } = req.query;
354
+ if (!address) return res.status(400).json({ error: "address query param required" });
355
+ try {
356
+ const registered = await client.isOracleRegistered(address);
357
+ res.json({ address, registered });
358
+ } catch (e) {
359
+ res.status(500).json({ error: e.message });
360
+ }
361
+ });
362
+
363
+ // ─── Shutdown ─────────────────────────────────────────────────────────────────
364
+
365
+ function shutdown(signal) {
366
+ log.info(`${signal} — shutting down`);
367
+ orderStore.destroy?.();
368
+ process.exit(0);
369
+ }
370
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
371
+ process.on("SIGINT", () => shutdown("SIGINT"));
372
+
373
+ // ─── Start ────────────────────────────────────────────────────────────────────
374
+
375
+ const PUBLIC_PORT = Number(process.env.PORT || 5000);
376
+ const ADMIN_PORT = Number(process.env.ADMIN_PORT || 5001);
377
+
378
+ publicApp.listen(PUBLIC_PORT, () => {
379
+ log.info("Public server started", { port: PUBLIC_PORT, bind: "0.0.0.0" });
380
+ log.info(`Razorpay: ${razorpay ? "configured" : "not configured"}`);
381
+ log.info(`Demo mode: ${DEMO_MODE}`);
382
+ });
383
+
384
+ adminApp.listen(ADMIN_PORT, "127.0.0.1", () => {
385
+ log.info("Admin server started", { port: ADMIN_PORT, bind: "127.0.0.1" });
386
+ if (IS_PRODUCTION && !process.env.ADMIN_API_KEY) {
387
+ log.error("ADMIN_API_KEY required in production");
388
+ process.exit(1);
389
+ }
390
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @algopayoracle/oracle-sdk — Quickstart
3
+ *
4
+ * Demonstrates anchor mode (no contract needed).
5
+ * A real payment proof is signed and anchored on Algorand TestNet.
6
+ *
7
+ * Run:
8
+ * ORACLE_MNEMONIC="your 25 words" node examples/quickstart.js
9
+ */
10
+
11
+ require("dotenv").config();
12
+ const { AlgoPayClient, OracleSigner } = require("../src");
13
+
14
+ async function main() {
15
+ const mnemonic = process.env.ORACLE_MNEMONIC;
16
+ if (!mnemonic) {
17
+ console.error("Set ORACLE_MNEMONIC in your environment or .env file");
18
+ process.exit(1);
19
+ }
20
+
21
+ // ── 1. Inspect oracle identity ─────────────────────────────────────────
22
+ const signer = new OracleSigner(mnemonic);
23
+ console.log("\n🔑 Oracle address :", signer.getAddress());
24
+ console.log("📦 Oracle pubkey :", signer.getPublicKeyBase64());
25
+ console.log(" (paste this into AlgoPayOracle.py create() call)\n");
26
+
27
+ // ── 2. Create client in anchor mode (no appId) ─────────────────────────
28
+ // Anchor mode: proof is stored in the note field of a 0-ALGO self-payment.
29
+ // Use this while testing before contract deployment.
30
+ const client = new AlgoPayClient({
31
+ mnemonic,
32
+ network: "testnet",
33
+ // appId: Number(process.env.ALGO_APP_ID), // uncomment after deployment
34
+ });
35
+
36
+ // ── 3. Sign a payment proof ────────────────────────────────────────────
37
+ const proof = client.signer.sign({
38
+ payment_id: "demo_" + Date.now(),
39
+ amount: 100,
40
+ action: "unlock",
41
+ currency: "INR",
42
+ });
43
+
44
+ console.log("✍️ Signed proof:");
45
+ console.log(JSON.stringify(proof, null, 2));
46
+
47
+ // ── 4. Verify offline (no network) ────────────────────────────────────
48
+ const offchainResult = client.verifyProofOffchain(proof);
49
+ console.log("\n🔍 Off-chain verify:", offchainResult.valid ? "✅ valid" : "❌ " + offchainResult.reason);
50
+
51
+ // ── 5. Submit to Algorand ─────────────────────────────────────────────
52
+ console.log("\n⛓ Submitting to Algorand TestNet...");
53
+ const result = await client.verifyAndCommit({
54
+ payment_id: proof.payment_id,
55
+ amount: proof.amount,
56
+ action: proof.action,
57
+ currency: proof.currency,
58
+ });
59
+
60
+ console.log("\n✅ Success!");
61
+ console.log(" txId :", result.txId);
62
+ console.log(" Explorer :", result.explorerUrl);
63
+ console.log("\n📋 APC-1 Credential:");
64
+ console.log(JSON.stringify(result.apc1, null, 2));
65
+ }
66
+
67
+ main().catch(e => { console.error("Error:", e.message); process.exit(1); });
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@algopayoracle/oracle-sdk",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Programmable payment oracle SDK — bridge fiat payments to Algorand smart contracts",
5
5
  "main": "src/index.js",
6
6
  "files": [
7
7
  "src",
8
+ "examples",
8
9
  "README.md"
9
10
  ],
10
11
  "scripts": {
@@ -86,7 +86,7 @@ class AlgoPayClient {
86
86
  * }>}
87
87
  */
88
88
  async verifyAndCommit({ payment_id, amount, action = "unlock", currency = "INR", provider = "unknown" }) {
89
- const proof = this.signer.sign({ payment_id, amount, action, currency, provider }, this.appId);
89
+ const proof = this.signer.sign({ payment_id, amount, action, currency, provider }, this.appId || 0);
90
90
  const txId = await this._submitProof(proof);
91
91
 
92
92
  return {