@algopayoracle/oracle-sdk 1.0.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 +397 -0
- package/package.json +60 -0
- package/src/AlgoPayClient.js +298 -0
- package/src/OracleSigner.js +142 -0
- package/src/ProofVerifier.js +132 -0
- package/src/adapters/index.js +4 -0
- package/src/adapters/razorpay.js +177 -0
- package/src/adapters/stripe.js +77 -0
- package/src/apc1.js +112 -0
- package/src/errors.js +96 -0
- package/src/index.d.ts +373 -0
- package/src/index.js +99 -0
- package/src/networks.js +66 -0
- package/src/utils/logger.js +98 -0
- package/src/utils/store.js +119 -0
- package/src/validate.js +105 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AlgoPay Oracle — Pluggable Order Store
|
|
3
|
+
*
|
|
4
|
+
* Stores server-authoritative payment amounts keyed by gateway order ID.
|
|
5
|
+
* This is the source of truth for what amount was agreed server-side —
|
|
6
|
+
* the client cannot override it in the verify-payment path.
|
|
7
|
+
*
|
|
8
|
+
* CURRENT IMPLEMENTATION: In-memory (Map with TTL sweep).
|
|
9
|
+
* - Fine for single-instance deployments and hackathon demos.
|
|
10
|
+
* - Loses state on restart.
|
|
11
|
+
* - Breaks in multi-instance / horizontally scaled deployments.
|
|
12
|
+
*
|
|
13
|
+
* ROADMAP — swap to a persistent backend by implementing the same interface:
|
|
14
|
+
*
|
|
15
|
+
* class RedisOrderStore {
|
|
16
|
+
* async set(id, data, ttlMs) { await redis.set(id, JSON.stringify(data), "PX", ttlMs); }
|
|
17
|
+
* async get(id) { const v = await redis.get(id); return v ? JSON.parse(v) : null; }
|
|
18
|
+
* async delete(id) { await redis.del(id); }
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* class PostgresOrderStore {
|
|
22
|
+
* async set(id, data, ttlMs) { await db.query("INSERT INTO orders ..."); }
|
|
23
|
+
* async get(id) { const r = await db.query("SELECT ..."); return r.rows[0] ?? null; }
|
|
24
|
+
* async delete(id) { await db.query("DELETE FROM orders WHERE id=$1", [id]); }
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* const { createOrderStore } = require("./store");
|
|
29
|
+
* const store = createOrderStore();
|
|
30
|
+
* await store.set("order_123", { amount: 100, currency: "INR" });
|
|
31
|
+
* const record = await store.get("order_123");
|
|
32
|
+
* await store.delete("order_123");
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
"use strict";
|
|
36
|
+
|
|
37
|
+
const ORDER_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
38
|
+
|
|
39
|
+
class InMemoryOrderStore {
|
|
40
|
+
constructor(ttlMs = ORDER_TTL_MS) {
|
|
41
|
+
this._map = new Map();
|
|
42
|
+
this._ttlMs = ttlMs;
|
|
43
|
+
|
|
44
|
+
// Sweep expired entries every 10 minutes
|
|
45
|
+
this._sweep = setInterval(() => {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
for (const [id, record] of this._map) {
|
|
48
|
+
if (now - record._createdAt > this._ttlMs) this._map.delete(id);
|
|
49
|
+
}
|
|
50
|
+
}, 10 * 60 * 1000);
|
|
51
|
+
|
|
52
|
+
// Don't keep the process alive just for sweeping
|
|
53
|
+
if (this._sweep.unref) this._sweep.unref();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Store an order record.
|
|
58
|
+
* @param {string} id - gateway order ID
|
|
59
|
+
* @param {object} data - { amount, currency, ... }
|
|
60
|
+
*/
|
|
61
|
+
async set(id, data) {
|
|
62
|
+
this._map.set(id, { ...data, _createdAt: Date.now() });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Retrieve an order record without consuming it.
|
|
67
|
+
* Returns null if not found or expired.
|
|
68
|
+
*/
|
|
69
|
+
async get(id) {
|
|
70
|
+
const record = this._map.get(id);
|
|
71
|
+
if (!record) return null;
|
|
72
|
+
if (Date.now() - record._createdAt > this._ttlMs) {
|
|
73
|
+
this._map.delete(id);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const { _createdAt, ...data } = record;
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retrieve and delete an order record (single-use consume).
|
|
82
|
+
* Returns null if not found or expired.
|
|
83
|
+
*/
|
|
84
|
+
async consume(id) {
|
|
85
|
+
const data = await this.get(id);
|
|
86
|
+
if (data) this._map.delete(id);
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Delete an order record explicitly.
|
|
92
|
+
*/
|
|
93
|
+
async delete(id) {
|
|
94
|
+
this._map.delete(id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @returns {number} current number of stored orders */
|
|
98
|
+
size() { return this._map.size; }
|
|
99
|
+
|
|
100
|
+
destroy() { clearInterval(this._sweep); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Factory — returns the configured store implementation.
|
|
105
|
+
*
|
|
106
|
+
* Currently always returns InMemoryOrderStore.
|
|
107
|
+
* Future: detect REDIS_URL or DATABASE_URL and return the appropriate adapter.
|
|
108
|
+
*
|
|
109
|
+
* @returns {InMemoryOrderStore}
|
|
110
|
+
*/
|
|
111
|
+
function createOrderStore(options = {}) {
|
|
112
|
+
if (process.env.REDIS_URL) {
|
|
113
|
+
// TODO: return new RedisOrderStore(process.env.REDIS_URL, options);
|
|
114
|
+
console.warn("[store] REDIS_URL detected but RedisOrderStore is not yet implemented — using in-memory store");
|
|
115
|
+
}
|
|
116
|
+
return new InMemoryOrderStore(options.ttlMs);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { InMemoryOrderStore, createOrderStore };
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AlgoPay Oracle SDK — Input Validation
|
|
3
|
+
*
|
|
4
|
+
* Lightweight schema validation for PaymentEvent and proof objects.
|
|
5
|
+
* No external dependencies — validates manually for zero overhead.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { validatePaymentEvent } = require("@algopayoracle/oracle-sdk/validate");
|
|
9
|
+
* const event = validatePaymentEvent(req.body); // throws ConfigError on invalid input
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const { ConfigError, InsufficientAmountError } = require("./errors");
|
|
15
|
+
|
|
16
|
+
const MIN_AMOUNT = 100;
|
|
17
|
+
const MAX_PAYMENT_ID = 256; // chars
|
|
18
|
+
const MAX_ACTION_LEN = 64;
|
|
19
|
+
const CURRENCY_RE = /^[A-Z]{3}$/; // ISO 4217: 3 uppercase letters
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate and normalise a PaymentEvent.
|
|
23
|
+
* Throws a typed error on the first validation failure.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} input
|
|
26
|
+
* @returns {import("./index").PaymentEvent} normalised PaymentEvent
|
|
27
|
+
* @throws {ConfigError | InsufficientAmountError}
|
|
28
|
+
*/
|
|
29
|
+
function validatePaymentEvent(input) {
|
|
30
|
+
if (!input || typeof input !== "object") {
|
|
31
|
+
throw new ConfigError("PaymentEvent must be an object");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { payment_id, amount, currency = "INR", action = "unlock", provider = "unknown" } = input;
|
|
35
|
+
|
|
36
|
+
// payment_id
|
|
37
|
+
if (!payment_id || typeof payment_id !== "string") {
|
|
38
|
+
throw new ConfigError("PaymentEvent.payment_id must be a non-empty string");
|
|
39
|
+
}
|
|
40
|
+
if (payment_id.length > MAX_PAYMENT_ID) {
|
|
41
|
+
throw new ConfigError(`PaymentEvent.payment_id must be ≤ ${MAX_PAYMENT_ID} characters`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// amount — must be a safe positive integer
|
|
45
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
46
|
+
throw new ConfigError("PaymentEvent.amount must be a positive integer");
|
|
47
|
+
}
|
|
48
|
+
if (amount < MIN_AMOUNT) {
|
|
49
|
+
throw new InsufficientAmountError(amount, MIN_AMOUNT);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// currency — 3-char ISO 4217 code
|
|
53
|
+
const normCurrency = String(currency).toUpperCase();
|
|
54
|
+
if (!CURRENCY_RE.test(normCurrency)) {
|
|
55
|
+
throw new ConfigError(`PaymentEvent.currency must be a 3-character ISO 4217 code (e.g. INR, USD), got: "${currency}"`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// action
|
|
59
|
+
if (!action || typeof action !== "string") {
|
|
60
|
+
throw new ConfigError("PaymentEvent.action must be a non-empty string");
|
|
61
|
+
}
|
|
62
|
+
if (action.length > MAX_ACTION_LEN) {
|
|
63
|
+
throw new ConfigError(`PaymentEvent.action must be ≤ ${MAX_ACTION_LEN} characters`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// timestamp sanity (if provided — internal proofs carry this)
|
|
67
|
+
if (input.timestamp !== undefined) {
|
|
68
|
+
const now = Math.floor(Date.now() / 1000);
|
|
69
|
+
if (!Number.isInteger(input.timestamp)) {
|
|
70
|
+
throw new ConfigError("PaymentEvent.timestamp must be an integer (unix seconds)");
|
|
71
|
+
}
|
|
72
|
+
if (input.timestamp > now + 60) {
|
|
73
|
+
throw new ConfigError(`PaymentEvent.timestamp is too far in the future: ${input.timestamp}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
payment_id,
|
|
79
|
+
amount,
|
|
80
|
+
currency: normCurrency,
|
|
81
|
+
action,
|
|
82
|
+
provider: typeof provider === "string" ? provider : "unknown",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate that a proof has all required fields for off-chain verification.
|
|
88
|
+
* Does not check the signature — use ProofVerifier for that.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} proof
|
|
91
|
+
* @throws {ConfigError}
|
|
92
|
+
*/
|
|
93
|
+
function validateProofFields(proof) {
|
|
94
|
+
const required = ["payment_id", "canonical_id", "amount", "currency", "action", "timestamp", "app_id", "oracle_address", "signature"];
|
|
95
|
+
for (const field of required) {
|
|
96
|
+
if (proof[field] == null) {
|
|
97
|
+
throw new ConfigError(`Proof is missing required field: ${field}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (proof.oracle_address.length !== 58) {
|
|
101
|
+
throw new ConfigError("Proof oracle_address must be a valid Algorand address (58 characters)");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { validatePaymentEvent, validateProofFields, MIN_AMOUNT };
|