@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.
@@ -0,0 +1,298 @@
1
+ /**
2
+ * AlgoPay Oracle SDK — AlgoPayClient
3
+ *
4
+ * Main entry point. Wraps OracleSigner + Algorand submission + ProofVerifier.
5
+ *
6
+ * Design:
7
+ * - Provider-agnostic at the credential layer: verifyAndCommit() accepts a
8
+ * PaymentEvent from any adapter (Razorpay, Stripe, UPI, manual).
9
+ * - The payment rail (how money moved) is the adapter's concern.
10
+ * - The credential (proof of payment) is always APC-1.
11
+ * - One canonical proof path — no legacy branches, no format drift.
12
+ *
13
+ * Quick start:
14
+ * const client = new AlgoPayClient({
15
+ * mnemonic: process.env.ORACLE_MNEMONIC,
16
+ * network: "testnet",
17
+ * appId: Number(process.env.ALGO_APP_ID),
18
+ * });
19
+ *
20
+ * const result = await client.verifyAndCommit({
21
+ * payment_id: "pay_XXXXXXX",
22
+ * amount: 100,
23
+ * action: "unlock",
24
+ * provider: "razorpay", // optional — enables cross-provider replay protection
25
+ * });
26
+ */
27
+
28
+ const algosdk = require("algosdk");
29
+ const { OracleSigner } = require("./OracleSigner");
30
+ const { ProofVerifier } = require("./ProofVerifier");
31
+ const { createClients } = require("./networks");
32
+ const { toAPC1 } = require("./apc1");
33
+ const { ConfigError } = require("./errors");
34
+
35
+ class AlgoPayClient {
36
+ /**
37
+ * @param {object} opts
38
+ * @param {string} opts.mnemonic - 25-word oracle account mnemonic
39
+ * @param {"localnet"|"testnet"|"mainnet"} [opts.network]
40
+ * @param {number} [opts.appId] - deployed AlgoPayOracle App ID
41
+ * @param {algosdk.Algodv2} [opts.algod] - custom algod (overrides network)
42
+ * @param {algosdk.Indexer} [opts.indexer] - custom indexer
43
+ * @param {string} [opts.explorerBase] - custom explorer URL
44
+ */
45
+ constructor({ mnemonic, network = "testnet", appId = null, algod, indexer, explorerBase } = {}) {
46
+ if (!mnemonic) throw new ConfigError("mnemonic is required");
47
+
48
+ this.network = network;
49
+ this.appId = appId;
50
+ this.signer = new OracleSigner(mnemonic);
51
+
52
+ if (algod && indexer) {
53
+ this.algod = algod;
54
+ this.indexer = indexer;
55
+ this.explorerBase = explorerBase || "";
56
+ } else {
57
+ const clients = createClients(network);
58
+ this.algod = clients.algod;
59
+ this.indexer = clients.indexer;
60
+ this.explorerBase = explorerBase || clients.config.explorerBase;
61
+ }
62
+
63
+ this.verifier = new ProofVerifier({ indexer: this.indexer, network });
64
+ }
65
+
66
+ // ── Core API ──────────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Sign a payment proof and commit it to Algorand.
70
+ * This is the only method most integrations need.
71
+ *
72
+ * @param {object} payment
73
+ * @param {string} payment.payment_id - provider-issued payment reference
74
+ * @param {number} payment.amount - integer fiat amount
75
+ * @param {string} [payment.action] - "unlock" | "mint" | "vote" | any string
76
+ * @param {string} [payment.currency] - ISO 4217 (default: "INR")
77
+ * @param {string} [payment.provider] - payment rail label (enables namespaced replay protection)
78
+ *
79
+ * @returns {Promise<{
80
+ * txId: string, — confirmed Algorand transaction ID
81
+ * proof: object, — internal signed proof
82
+ * apc1: object, — APC-1 standardized credential
83
+ * explorerUrl: string,
84
+ * verifyUrl: string,
85
+ * access_seconds: number
86
+ * }>}
87
+ */
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);
90
+ const txId = await this._submitProof(proof);
91
+
92
+ return {
93
+ txId,
94
+ proof,
95
+ apc1: toAPC1(proof, { network: this.network, appId: this.appId, provider }),
96
+ explorerUrl: `${this.explorerBase}/transaction/${txId}`,
97
+ verifyUrl: `/verify-proof/${txId}`,
98
+ access_seconds: 300,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Verify a proof by txId (indexer lookup).
104
+ * @param {string} txId
105
+ * @param {object} [opts]
106
+ */
107
+ async verifyProof(txId, opts = {}) {
108
+ return this.verifier.verifyTxn(txId, opts);
109
+ }
110
+
111
+ /**
112
+ * Verify a proof object offline (no network).
113
+ * @param {object} proof
114
+ * @param {object} [opts]
115
+ */
116
+ verifyProofOffchain(proof, opts = {}) {
117
+ return this.verifier.verifyOffchain(proof, opts);
118
+ }
119
+
120
+ // ── Oracle rotation ───────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Register a new oracle in the contract. Creator-only.
124
+ * @param {string} addressOrBase64 - Algorand address or base64 pubkey
125
+ * @returns {Promise<string>} txId
126
+ */
127
+ async addOracle(addressOrBase64) {
128
+ if (!this.appId) throw new ConfigError("appId required for oracle management");
129
+ return this._oracleAdminCall("add_oracle", AlgoPayClient._toPubKeyBytes(addressOrBase64));
130
+ }
131
+
132
+ /**
133
+ * Deregister an oracle. Creator-only. Cannot remove the last oracle.
134
+ * @param {string} addressOrBase64
135
+ * @returns {Promise<string>} txId
136
+ */
137
+ async removeOracle(addressOrBase64) {
138
+ if (!this.appId) throw new ConfigError("appId required for oracle management");
139
+ return this._oracleAdminCall("remove_oracle", AlgoPayClient._toPubKeyBytes(addressOrBase64));
140
+ }
141
+
142
+ /**
143
+ * Check if an oracle pubkey is registered in the contract.
144
+ * @param {string} addressOrBase64
145
+ * @returns {Promise<boolean>}
146
+ */
147
+ async isOracleRegistered(addressOrBase64) {
148
+ if (!this.appId) throw new ConfigError("appId required");
149
+ const pubkey = AlgoPayClient._toPubKeyBytes(addressOrBase64);
150
+ const params = await this.algod.getTransactionParams().do();
151
+ const method = new algosdk.ABIMethod({
152
+ name: "is_oracle", args: [{ name: "pubkey", type: "byte[]" }], returns: { type: "bool" },
153
+ });
154
+ const signer = algosdk.makeBasicAccountTransactionSigner(this.signer.account);
155
+ const atc = new algosdk.AtomicTransactionComposer();
156
+ atc.addMethodCall({
157
+ appID: this.appId, method,
158
+ methodArgs: [Buffer.from(pubkey)],
159
+ boxes: [{ appIndex: 0, name: Buffer.from(pubkey) }],
160
+ sender: this.signer.address, suggestedParams: params, signer,
161
+ });
162
+ const result = await atc.simulate(this.algod);
163
+ return result.methodResults[0].returnValue;
164
+ }
165
+
166
+ // ── Contract stats ────────────────────────────────────────────────────────
167
+
168
+ async getTotalVerified() {
169
+ if (!this.appId) throw new ConfigError("appId required");
170
+ const info = await this.algod.getApplicationByID(this.appId).do();
171
+ const state = info.params["global-state"] || [];
172
+ const entry = state.find(s => Buffer.from(s.key, "base64").toString() === "total_verified");
173
+ return entry ? Number(entry.value.uint) : 0;
174
+ }
175
+
176
+ async getOracleCount() {
177
+ if (!this.appId) throw new ConfigError("appId required");
178
+ const info = await this.algod.getApplicationByID(this.appId).do();
179
+ const state = info.params["global-state"] || [];
180
+ const entry = state.find(s => Buffer.from(s.key, "base64").toString() === "oracle_count");
181
+ return entry ? Number(entry.value.uint) : 0;
182
+ }
183
+
184
+ // ── Identity ──────────────────────────────────────────────────────────────
185
+
186
+ getAddress() { return this.signer.address; }
187
+ getPublicKeyBase64() { return this.signer.getPublicKeyBase64(); }
188
+ getPublicKeyBytes() { return this.signer.getPublicKeyBytes(); }
189
+ getExplorerUrl(txId) { return `${this.explorerBase}/transaction/${txId}`; }
190
+
191
+ // ── Internal ──────────────────────────────────────────────────────────────
192
+
193
+ async _submitProof(proof) {
194
+ const params = await this.algod.getTransactionParams().do();
195
+ params.flatFee = true;
196
+ params.fee = Math.max(Number(params.minFee ?? 1000), 1000) * 8;
197
+
198
+ // proof JSON in note = the anchor. Indexer picks this up for verifyProof().
199
+ const note = new TextEncoder().encode(JSON.stringify(proof));
200
+
201
+ if (this.appId) {
202
+ const oraclePubKeyBytes = this.signer.getPublicKeyBytes();
203
+
204
+ const verifyMethod = new algosdk.ABIMethod({
205
+ name: "verify_payment",
206
+ args: [
207
+ { name: "payment_id", type: "string" },
208
+ { name: "action", type: "string" },
209
+ { name: "amount", type: "uint64" },
210
+ { name: "currency", type: "string" },
211
+ { name: "timestamp", type: "uint64" },
212
+ { name: "app_id", type: "uint64" },
213
+ { name: "oracle_pubkey", type: "byte[]" },
214
+ { name: "signature", type: "byte[]" },
215
+ ],
216
+ returns: { type: "bool" },
217
+ });
218
+
219
+ const nopMethod = new algosdk.ABIMethod({
220
+ name: "nop", args: [], returns: { type: "void" },
221
+ });
222
+
223
+ const signer = algosdk.makeBasicAccountTransactionSigner(this.signer.account);
224
+ const atc = new algosdk.AtomicTransactionComposer();
225
+
226
+ for (let i = 1; i <= 3; i++) {
227
+ atc.addMethodCall({
228
+ appID: this.appId, method: nopMethod, methodArgs: [],
229
+ sender: this.signer.address, suggestedParams: params,
230
+ note: new TextEncoder().encode(`pad${i}`), signer,
231
+ });
232
+ }
233
+
234
+ atc.addMethodCall({
235
+ appID: this.appId,
236
+ method: verifyMethod,
237
+ methodArgs: [
238
+ proof.canonical_id, // namespaced box key
239
+ proof.action,
240
+ proof.amount,
241
+ proof.currency,
242
+ proof.timestamp,
243
+ this.appId,
244
+ Buffer.from(oraclePubKeyBytes),
245
+ Buffer.from(proof.signature, "base64"),
246
+ ],
247
+ boxes: [
248
+ { appIndex: 0, name: Buffer.from(oraclePubKeyBytes) }, // oracle registry
249
+ { appIndex: 0, name: new TextEncoder().encode(proof.canonical_id) }, // replay lock
250
+ ],
251
+ sender: this.signer.address, suggestedParams: params, note, signer,
252
+ });
253
+
254
+ const result = await atc.execute(this.algod, 6);
255
+ return result.txIDs[3];
256
+
257
+ } else {
258
+ // Anchor mode — 0-ALGO self-payment, proof JSON in note field
259
+ const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
260
+ sender: this.signer.address, receiver: this.signer.address, amount: 0,
261
+ note, suggestedParams: params,
262
+ });
263
+ const signed = txn.signTxn(this.signer.account.sk);
264
+ const { txid } = await this.algod.sendRawTransaction(signed).do();
265
+ await algosdk.waitForConfirmation(this.algod, txid, 6);
266
+ return txid;
267
+ }
268
+ }
269
+
270
+ async _oracleAdminCall(methodName, pubkeyBytes) {
271
+ const params = await this.algod.getTransactionParams().do();
272
+ params.flatFee = true;
273
+ params.fee = Math.max(Number(params.minFee ?? 1000), 1000) * 2;
274
+
275
+ const method = new algosdk.ABIMethod({
276
+ name: methodName, args: [{ name: "pubkey", type: "byte[]" }], returns: { type: "void" },
277
+ });
278
+ const signer = algosdk.makeBasicAccountTransactionSigner(this.signer.account);
279
+ const atc = new algosdk.AtomicTransactionComposer();
280
+ atc.addMethodCall({
281
+ appID: this.appId, method,
282
+ methodArgs: [Buffer.from(pubkeyBytes)],
283
+ boxes: [{ appIndex: 0, name: Buffer.from(pubkeyBytes) }],
284
+ sender: this.signer.address, suggestedParams: params, signer,
285
+ });
286
+ const result = await atc.execute(this.algod, 4);
287
+ return result.txIDs[0];
288
+ }
289
+
290
+ static _toPubKeyBytes(addressOrBase64) {
291
+ if (typeof addressOrBase64 === "string" && addressOrBase64.length === 58) {
292
+ return algosdk.decodeAddress(addressOrBase64).publicKey;
293
+ }
294
+ return Buffer.from(addressOrBase64, "base64");
295
+ }
296
+ }
297
+
298
+ module.exports = { AlgoPayClient };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * AlgoPay Oracle SDK — OracleSigner
3
+ *
4
+ * Pure Ed25519 signing — no network calls.
5
+ * The credential layer is provider-agnostic: sign() accepts a PaymentEvent
6
+ * from any adapter and produces an APC-1 compatible signed proof.
7
+ *
8
+ * Replay protection namespacing:
9
+ * On-chain box keys use `provider:payment_id` (canonical_id), not raw payment_id.
10
+ * This prevents cross-provider collisions where two providers issue the same ID.
11
+ * The contract only sees canonical_id — it does not know or care about providers.
12
+ */
13
+
14
+ const algosdk = require("algosdk");
15
+ const { ConfigError, InsufficientAmountError } = require("./errors");
16
+ const { validatePaymentEvent } = require("./validate");
17
+
18
+ const PROOF_PREFIX = "AlgoPay:v1:";
19
+ const MIN_AMOUNT = 100;
20
+
21
+ class OracleSigner {
22
+ /** @param {string} mnemonic - 25-word Algorand mnemonic */
23
+ constructor(mnemonic) {
24
+ if (!mnemonic) throw new ConfigError("mnemonic is required");
25
+ try {
26
+ this.account = algosdk.mnemonicToSecretKey(mnemonic);
27
+ } catch (e) {
28
+ throw new ConfigError(`Invalid mnemonic: ${e.message}`);
29
+ }
30
+ this.address = this.account.addr.toString();
31
+ }
32
+
33
+ /**
34
+ * Sign a payment proof.
35
+ *
36
+ * @param {object} payload
37
+ * @param {string} payload.payment_id - provider-issued payment reference
38
+ * @param {number} payload.amount - integer fiat amount (e.g. 100 for ₹100)
39
+ * @param {string} [payload.action] - intended Web3 action (default: "unlock")
40
+ * @param {string} [payload.currency] - ISO 4217 (default: "INR")
41
+ * @param {string} [payload.provider] - payment provider label (default: "unknown")
42
+ * @returns {object} signed proof — pass directly to AlgoPayClient._submitProof()
43
+ */
44
+ sign({ payment_id, amount, action = "unlock", currency = "INR", provider = "unknown"}, appId = 0) {
45
+ // Delegate validation to validatePaymentEvent — consistent typed errors
46
+ validatePaymentEvent({ payment_id, amount, action, currency, provider });
47
+
48
+ // canonical_id = namespaced box key used on-chain for replay protection.
49
+ // Prevents cross-provider replay: razorpay:pay_ABC != stripe:pay_ABC.
50
+ // "unknown" provider falls back to raw payment_id (demo/manual mode).
51
+ const canonical_id = provider !== "unknown"
52
+ ? `${provider}:${payment_id}`
53
+ : payment_id;
54
+
55
+ const timestamp = Math.floor(Date.now() / 1000) - 30; // backdate 30s — absorbs signing + tx propagation latency
56
+ const message = OracleSigner.buildMessage(canonical_id, action, currency, amount, timestamp, appId);
57
+ const sigBytes = algosdk.signBytes(message, this.account.sk);
58
+
59
+ return {
60
+ payment_id, // original provider ID — for display and logging
61
+ canonical_id, // namespaced — used in signing and as on-chain box key
62
+ provider,
63
+ amount,
64
+ action,
65
+ currency: currency.toUpperCase(),
66
+ timestamp,
67
+ app_id: appId,
68
+ oracle_address: this.address,
69
+ signature: Buffer.from(sigBytes).toString("base64"),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Build the raw message bytes for signing.
75
+ *
76
+ * Byte order (must match AlgoPayOracle.py verify_payment exactly):
77
+ * AlgoPay:v1: + canonical_id + action + currency + amount(8B BE) + timestamp(8B BE)
78
+ *
79
+ * algosdk.signBytes prepends "MX" before the Ed25519 operation.
80
+ * The contract prepends "MX" manually before ed25519verify_bare.
81
+ * So the effective signed bytes are: MX + the message below.
82
+ *
83
+ * @param {string} canonical_id - namespaced payment ID (provider:payment_id)
84
+ * @param {string} action
85
+ * @param {string} currency
86
+ * @param {number} amount
87
+ * @param {number} timestamp - unix seconds
88
+ * @returns {Uint8Array}
89
+ */
90
+ static buildMessage(canonical_id, action, currency, amount, timestamp, appId = 0) {
91
+ const enc = new TextEncoder();
92
+ const parts = [
93
+ enc.encode(PROOF_PREFIX),
94
+ enc.encode(canonical_id),
95
+ enc.encode(action),
96
+ enc.encode(currency.toUpperCase()),
97
+ algosdk.encodeUint64(amount),
98
+ algosdk.encodeUint64(timestamp),
99
+ algosdk.encodeUint64(appId),
100
+ ];
101
+ const total = parts.reduce((s, p) => s + p.length, 0);
102
+ const msg = new Uint8Array(total);
103
+ let off = 0;
104
+ for (const p of parts) { msg.set(p, off); off += p.length; }
105
+ return msg;
106
+ }
107
+
108
+ /**
109
+ * Verify a proof's signature offline — no network, no indexer.
110
+ * Uses canonical_id if present, falls back to payment_id for backward compat.
111
+ *
112
+ * @param {object} proof
113
+ * @returns {boolean}
114
+ */
115
+ static verifyOffchain(proof) {
116
+ try {
117
+ const id = proof.canonical_id || proof.payment_id;
118
+ const message = OracleSigner.buildMessage(
119
+ id, proof.action, proof.currency || "INR", proof.amount, proof.timestamp, proof.app_id || 0
120
+ );
121
+ const sigBytes = Buffer.from(proof.signature, "base64");
122
+ return algosdk.verifyBytes(message, sigBytes, proof.oracle_address);
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /** @returns {string} Base64 pubkey — paste into create() call at contract deploy */
129
+ getPublicKeyBase64() {
130
+ return Buffer.from(algosdk.decodeAddress(this.address).publicKey).toString("base64");
131
+ }
132
+
133
+ /** @returns {Uint8Array} Raw 32-byte Ed25519 public key */
134
+ getPublicKeyBytes() {
135
+ return algosdk.decodeAddress(this.address).publicKey;
136
+ }
137
+
138
+ /** @returns {string} Algorand address of this oracle */
139
+ getAddress() { return this.address; }
140
+ }
141
+
142
+ module.exports = { OracleSigner };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * AlgoPay Oracle SDK — ProofVerifier
3
+ *
4
+ * Verifies APC-1 proofs both off-chain (Ed25519 sig check) and
5
+ * on-chain (Algorand indexer transaction lookup).
6
+ *
7
+ * Verification chain:
8
+ * 1. APC-1 version supported
9
+ * 2. Required fields present and typed correctly
10
+ * 3. canonical_id format sane (provider:payment_id or raw id)
11
+ * 4. Proof not expired
12
+ * 5. Oracle address filter (optional)
13
+ * 6. Action filter (optional)
14
+ * 7. Ed25519 signature verified using canonical_id (not payment_id)
15
+ */
16
+
17
+ "use strict";
18
+
19
+ const { OracleSigner } = require("./OracleSigner");
20
+ const { validateAPC1Structure, isSupportedVersion, isExpired } = require("./apc1");
21
+ const { InvalidSignatureError, ProofExpiredError } = require("./errors");
22
+
23
+ class ProofVerifier {
24
+ /**
25
+ * @param {object} opts
26
+ * @param {object} opts.indexer - algosdk.Indexer instance
27
+ * @param {string} [opts.network]
28
+ */
29
+ constructor({ indexer, network = "testnet" } = {}) {
30
+ this.indexer = indexer;
31
+ this.network = network;
32
+ }
33
+
34
+ /**
35
+ * Verify an APC-1 proof object offline — no network calls.
36
+ *
37
+ * @param {object} proof
38
+ * @param {object} [opts]
39
+ * @param {string} [opts.expectedOracleAddress]
40
+ * @param {string} [opts.expectedAction]
41
+ * @param {number} [opts.maxAgeSecs] - default 300
42
+ * @returns {{ valid: boolean, reason?: string, proof?: object }}
43
+ */
44
+ verifyOffchain(proof, opts = {}) {
45
+ // 1. APC-1 version check — fail early on unknown versions
46
+ if (proof.apc && !isSupportedVersion(proof)) {
47
+ return { valid: false, reason: `unsupported APC version: "${proof.apc}"` };
48
+ }
49
+
50
+ // 2. Required fields (works for both full APC-1 and internal proof objects)
51
+ const required = ["payment_id", "canonical_id", "amount", "currency", "action", "timestamp", "oracle_address", "signature"];
52
+ for (const f of required) {
53
+ if (proof[f] == null) return { valid: false, reason: `missing required field: ${f}` };
54
+ }
55
+
56
+ // 3. canonical_id consistency check
57
+ // If provider is set and not "unknown", canonical_id should start with "provider:"
58
+ if (proof.provider && proof.provider !== "unknown") {
59
+ const expected_prefix = `${proof.provider}:`;
60
+ if (!proof.canonical_id.startsWith(expected_prefix) && proof.canonical_id !== proof.payment_id) {
61
+ return { valid: false, reason: `canonical_id "${proof.canonical_id}" does not match provider "${proof.provider}"` };
62
+ }
63
+ }
64
+
65
+ // 4. Expiry
66
+ const maxAge = opts.maxAgeSecs ?? 300;
67
+ if (isExpired(proof, maxAge)) {
68
+ const age = Math.floor(Date.now() / 1000) - proof.timestamp;
69
+ return { valid: false, reason: `proof expired — age ${age}s exceeds maximum ${maxAge}s` };
70
+ }
71
+
72
+ // 5. Oracle address filter
73
+ if (opts.expectedOracleAddress && proof.oracle_address !== opts.expectedOracleAddress) {
74
+ return { valid: false, reason: `oracle address mismatch: expected ${opts.expectedOracleAddress}` };
75
+ }
76
+
77
+ // 6. Action filter
78
+ if (opts.expectedAction && proof.action !== opts.expectedAction) {
79
+ return { valid: false, reason: `action mismatch: expected "${opts.expectedAction}", got "${proof.action}"` };
80
+ }
81
+
82
+ // 7. Ed25519 signature — uses canonical_id (the on-chain replay key)
83
+ const sigValid = OracleSigner.verifyOffchain(proof);
84
+ if (!sigValid) return { valid: false, reason: "Ed25519 signature verification failed" };
85
+
86
+ return { valid: true, proof };
87
+ }
88
+
89
+ /**
90
+ * Verify a proof by fetching its anchor transaction from the Algorand indexer.
91
+ * The proof JSON is stored in the transaction's note field.
92
+ *
93
+ * @param {string} txId
94
+ * @param {object} [opts] - same as verifyOffchain opts
95
+ * @returns {Promise<{ valid: boolean, reason?: string, proof?: object, txId: string }>}
96
+ */
97
+ async verifyTxn(txId, opts = {}) {
98
+ if (!this.indexer) return { valid: false, reason: "indexer not configured", txId };
99
+
100
+ let info;
101
+ try {
102
+ info = await this.indexer.lookupTransactionByID(txId).do();
103
+ } catch (e) {
104
+ return { valid: false, reason: `indexer lookup failed: ${e.message}`, txId };
105
+ }
106
+
107
+ const txn = info.transaction;
108
+ if (!txn?.note) return { valid: false, reason: "transaction not found or has no note field", txId };
109
+
110
+ let proof;
111
+ try {
112
+ proof = JSON.parse(Buffer.from(txn.note, "base64").toString("utf8"));
113
+ } catch {
114
+ return { valid: false, reason: "note field is not valid JSON", txId };
115
+ }
116
+
117
+ const result = this.verifyOffchain(proof, opts);
118
+ return { ...result, txId };
119
+ }
120
+
121
+ /**
122
+ * Batch verify multiple txIds.
123
+ * @param {string[]} txIds
124
+ * @param {object} [opts]
125
+ * @returns {Promise<Array>}
126
+ */
127
+ async verifyBatch(txIds, opts = {}) {
128
+ return Promise.all(txIds.map(id => this.verifyTxn(id, opts)));
129
+ }
130
+ }
131
+
132
+ module.exports = { ProofVerifier };
@@ -0,0 +1,4 @@
1
+ const { RazorpayAdapter } = require("./razorpay");
2
+ const { StripeAdapter } = require("./stripe");
3
+
4
+ module.exports = { RazorpayAdapter, StripeAdapter };