@vera-pay/sdk 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/dist/index.js ADDED
@@ -0,0 +1,1409 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ CADENCE_HANDLERS: () => CADENCE_HANDLERS,
34
+ DEFAULT_IPFS_GATEWAY: () => DEFAULT_IPFS_GATEWAY,
35
+ DEPLOYED_CONTRACTS: () => DEPLOYED_CONTRACTS,
36
+ ERC20_ABI: () => ERC20_ABI,
37
+ FlowScheduler: () => FlowScheduler,
38
+ KNOWN_TOKENS: () => KNOWN_TOKENS,
39
+ NETWORKS: () => NETWORKS,
40
+ VERA_PAY_ABI: () => VERA_PAY_ABI,
41
+ VeraPayClient: () => VeraPayClient,
42
+ buildPaymentReceipt: () => buildPaymentReceipt,
43
+ createKuboAdapter: () => createKuboAdapter,
44
+ createMemoryAdapter: () => createMemoryAdapter,
45
+ createStorachaAdapter: () => createStorachaAdapter,
46
+ ipfsGatewayUrl: () => ipfsGatewayUrl
47
+ });
48
+ module.exports = __toCommonJS(index_exports);
49
+
50
+ // src/client.ts
51
+ var import_ethers = require("ethers");
52
+
53
+ // src/abi.ts
54
+ var VERA_PAY_ABI = [
55
+ {
56
+ type: "constructor",
57
+ inputs: [
58
+ { name: "_feeRecipient", type: "address" },
59
+ { name: "_feeBps", type: "uint256" }
60
+ ],
61
+ stateMutability: "nonpayable"
62
+ },
63
+ {
64
+ type: "function",
65
+ name: "createPlan",
66
+ inputs: [
67
+ { name: "_paymentToken", type: "address" },
68
+ { name: "_amount", type: "uint256" },
69
+ { name: "_interval", type: "uint256" },
70
+ { name: "_name", type: "string" },
71
+ { name: "_metadataURI", type: "string" }
72
+ ],
73
+ outputs: [{ name: "planId", type: "uint256" }],
74
+ stateMutability: "nonpayable"
75
+ },
76
+ {
77
+ type: "function",
78
+ name: "togglePlan",
79
+ inputs: [{ name: "_planId", type: "uint256" }],
80
+ outputs: [],
81
+ stateMutability: "nonpayable"
82
+ },
83
+ {
84
+ type: "function",
85
+ name: "subscribe",
86
+ inputs: [{ name: "_planId", type: "uint256" }],
87
+ outputs: [{ name: "subId", type: "uint256" }],
88
+ stateMutability: "nonpayable"
89
+ },
90
+ {
91
+ type: "function",
92
+ name: "cancelSubscription",
93
+ inputs: [{ name: "_subId", type: "uint256" }],
94
+ outputs: [],
95
+ stateMutability: "nonpayable"
96
+ },
97
+ {
98
+ type: "function",
99
+ name: "processPayment",
100
+ inputs: [{ name: "_subId", type: "uint256" }],
101
+ outputs: [],
102
+ stateMutability: "nonpayable"
103
+ },
104
+ {
105
+ type: "function",
106
+ name: "batchProcessPayments",
107
+ inputs: [{ name: "_subIds", type: "uint256[]" }],
108
+ outputs: [],
109
+ stateMutability: "nonpayable"
110
+ },
111
+ {
112
+ type: "function",
113
+ name: "getPlan",
114
+ inputs: [{ name: "_planId", type: "uint256" }],
115
+ outputs: [
116
+ {
117
+ name: "",
118
+ type: "tuple",
119
+ components: [
120
+ { name: "merchant", type: "address" },
121
+ { name: "paymentToken", type: "address" },
122
+ { name: "amount", type: "uint256" },
123
+ { name: "interval", type: "uint256" },
124
+ { name: "name", type: "string" },
125
+ { name: "metadataURI", type: "string" },
126
+ { name: "active", type: "bool" }
127
+ ]
128
+ }
129
+ ],
130
+ stateMutability: "view"
131
+ },
132
+ {
133
+ type: "function",
134
+ name: "getSubscription",
135
+ inputs: [{ name: "_subId", type: "uint256" }],
136
+ outputs: [
137
+ {
138
+ name: "",
139
+ type: "tuple",
140
+ components: [
141
+ { name: "planId", type: "uint256" },
142
+ { name: "subscriber", type: "address" },
143
+ { name: "startTime", type: "uint256" },
144
+ { name: "lastPaymentTime", type: "uint256" },
145
+ { name: "paymentsCount", type: "uint256" },
146
+ { name: "active", type: "bool" }
147
+ ]
148
+ }
149
+ ],
150
+ stateMutability: "view"
151
+ },
152
+ {
153
+ type: "function",
154
+ name: "getMerchantPlans",
155
+ inputs: [{ name: "_merchant", type: "address" }],
156
+ outputs: [{ name: "", type: "uint256[]" }],
157
+ stateMutability: "view"
158
+ },
159
+ {
160
+ type: "function",
161
+ name: "getSubscriberSubscriptions",
162
+ inputs: [{ name: "_subscriber", type: "address" }],
163
+ outputs: [{ name: "", type: "uint256[]" }],
164
+ stateMutability: "view"
165
+ },
166
+ {
167
+ type: "function",
168
+ name: "isPaymentDue",
169
+ inputs: [{ name: "_subId", type: "uint256" }],
170
+ outputs: [{ name: "", type: "bool" }],
171
+ stateMutability: "view"
172
+ },
173
+ {
174
+ type: "function",
175
+ name: "getDuePayments",
176
+ inputs: [{ name: "_subIds", type: "uint256[]" }],
177
+ outputs: [{ name: "due", type: "uint256[]" }],
178
+ stateMutability: "view"
179
+ },
180
+ {
181
+ type: "function",
182
+ name: "nextPlanId",
183
+ inputs: [],
184
+ outputs: [{ name: "", type: "uint256" }],
185
+ stateMutability: "view"
186
+ },
187
+ {
188
+ type: "function",
189
+ name: "nextSubscriptionId",
190
+ inputs: [],
191
+ outputs: [{ name: "", type: "uint256" }],
192
+ stateMutability: "view"
193
+ },
194
+ {
195
+ type: "function",
196
+ name: "protocolFeeBps",
197
+ inputs: [],
198
+ outputs: [{ name: "", type: "uint256" }],
199
+ stateMutability: "view"
200
+ },
201
+ {
202
+ type: "function",
203
+ name: "activeSub",
204
+ inputs: [
205
+ { name: "planId", type: "uint256" },
206
+ { name: "subscriber", type: "address" }
207
+ ],
208
+ outputs: [{ name: "subId", type: "uint256" }],
209
+ stateMutability: "view"
210
+ },
211
+ {
212
+ type: "event",
213
+ name: "PlanCreated",
214
+ inputs: [
215
+ { name: "planId", type: "uint256", indexed: true },
216
+ { name: "merchant", type: "address", indexed: true },
217
+ { name: "paymentToken", type: "address", indexed: false },
218
+ { name: "amount", type: "uint256", indexed: false },
219
+ { name: "interval", type: "uint256", indexed: false },
220
+ { name: "name", type: "string", indexed: false },
221
+ { name: "metadataURI", type: "string", indexed: false }
222
+ ],
223
+ anonymous: false
224
+ },
225
+ {
226
+ type: "event",
227
+ name: "PlanToggled",
228
+ inputs: [
229
+ { name: "planId", type: "uint256", indexed: true },
230
+ { name: "active", type: "bool", indexed: false }
231
+ ],
232
+ anonymous: false
233
+ },
234
+ {
235
+ type: "event",
236
+ name: "Subscribed",
237
+ inputs: [
238
+ { name: "subscriptionId", type: "uint256", indexed: true },
239
+ { name: "planId", type: "uint256", indexed: true },
240
+ { name: "subscriber", type: "address", indexed: true }
241
+ ],
242
+ anonymous: false
243
+ },
244
+ {
245
+ type: "event",
246
+ name: "PaymentProcessed",
247
+ inputs: [
248
+ { name: "subscriptionId", type: "uint256", indexed: true },
249
+ { name: "planId", type: "uint256", indexed: true },
250
+ { name: "subscriber", type: "address", indexed: true },
251
+ { name: "merchant", type: "address", indexed: false },
252
+ { name: "amount", type: "uint256", indexed: false },
253
+ { name: "protocolFee", type: "uint256", indexed: false },
254
+ { name: "timestamp", type: "uint256", indexed: false }
255
+ ],
256
+ anonymous: false
257
+ },
258
+ {
259
+ type: "event",
260
+ name: "SubscriptionCancelled",
261
+ inputs: [
262
+ { name: "subscriptionId", type: "uint256", indexed: true },
263
+ { name: "subscriber", type: "address", indexed: true }
264
+ ],
265
+ anonymous: false
266
+ }
267
+ ];
268
+ var ERC20_ABI = [
269
+ {
270
+ type: "function",
271
+ name: "approve",
272
+ inputs: [
273
+ { name: "spender", type: "address" },
274
+ { name: "amount", type: "uint256" }
275
+ ],
276
+ outputs: [{ name: "", type: "bool" }],
277
+ stateMutability: "nonpayable"
278
+ },
279
+ {
280
+ type: "function",
281
+ name: "allowance",
282
+ inputs: [
283
+ { name: "owner", type: "address" },
284
+ { name: "spender", type: "address" }
285
+ ],
286
+ outputs: [{ name: "", type: "uint256" }],
287
+ stateMutability: "view"
288
+ },
289
+ {
290
+ type: "function",
291
+ name: "balanceOf",
292
+ inputs: [{ name: "account", type: "address" }],
293
+ outputs: [{ name: "", type: "uint256" }],
294
+ stateMutability: "view"
295
+ },
296
+ {
297
+ type: "function",
298
+ name: "decimals",
299
+ inputs: [],
300
+ outputs: [{ name: "", type: "uint8" }],
301
+ stateMutability: "view"
302
+ },
303
+ {
304
+ type: "function",
305
+ name: "symbol",
306
+ inputs: [],
307
+ outputs: [{ name: "", type: "string" }],
308
+ stateMutability: "view"
309
+ },
310
+ {
311
+ type: "function",
312
+ name: "mint",
313
+ inputs: [
314
+ { name: "to", type: "address" },
315
+ { name: "amount", type: "uint256" }
316
+ ],
317
+ outputs: [],
318
+ stateMutability: "nonpayable"
319
+ }
320
+ ];
321
+
322
+ // src/constants.ts
323
+ var NETWORKS = {
324
+ "flow-testnet": {
325
+ chainId: 545,
326
+ rpcUrl: "https://testnet.evm.nodes.onflow.org",
327
+ blockExplorer: "https://testnet.explorer.flow.com",
328
+ evmBlockExplorer: "https://testnet.evm.flow.com",
329
+ name: "Flow EVM Testnet"
330
+ },
331
+ "flow-mainnet": {
332
+ chainId: 747,
333
+ rpcUrl: "https://mainnet.evm.nodes.onflow.org",
334
+ blockExplorer: "https://explorer.flow.com",
335
+ evmBlockExplorer: "https://evm.flow.com",
336
+ name: "Flow EVM Mainnet"
337
+ }
338
+ };
339
+ var DEPLOYED_CONTRACTS = {
340
+ "flow-testnet": "0x24730C8387C11e6031f692Bf0B14000D93271766"
341
+ };
342
+ var KNOWN_TOKENS = {
343
+ "flow-testnet": {
344
+ USDC: "0x9C080703256BDF9Ea1b485aE72f13E31f74C558b",
345
+ "USDC.e": "0x9B7550D337bB449b89C6f9C926C3b976b6f4095b"
346
+ },
347
+ "flow-mainnet": {
348
+ USDT: "0x674843C06FF83502ddb4D37c2E09C01cdA38cbc8"
349
+ }
350
+ };
351
+ var CADENCE_HANDLERS = {
352
+ "flow-testnet": "7c0bf27829276c6b"
353
+ };
354
+ var DEFAULT_IPFS_GATEWAY = "https://storacha.link/ipfs";
355
+
356
+ // src/ipfs.ts
357
+ function createKuboAdapter(apiUrl) {
358
+ return {
359
+ async uploadJson(data) {
360
+ const blob = new Blob([JSON.stringify(data)], {
361
+ type: "application/json"
362
+ });
363
+ const form = new FormData();
364
+ form.append("file", blob, "receipt.json");
365
+ const res = await fetch(`${apiUrl}/api/v0/add?pin=true`, {
366
+ method: "POST",
367
+ body: form
368
+ });
369
+ if (!res.ok) throw new Error(`IPFS upload failed: ${res.statusText}`);
370
+ const result = await res.json();
371
+ return result.Hash;
372
+ },
373
+ async fetchJson(cid) {
374
+ const res = await fetch(`${DEFAULT_IPFS_GATEWAY}/${cid}`);
375
+ if (!res.ok) throw new Error(`IPFS fetch failed: ${res.statusText}`);
376
+ return res.json();
377
+ }
378
+ };
379
+ }
380
+ function createStorachaAdapter(clientOrConfig) {
381
+ let resolvedClient = null;
382
+ let initPromise = null;
383
+ async function getClient() {
384
+ if (resolvedClient) return resolvedClient;
385
+ if ("uploadFile" in clientOrConfig) {
386
+ resolvedClient = clientOrConfig;
387
+ return resolvedClient;
388
+ }
389
+ if (!initPromise) {
390
+ initPromise = (async () => {
391
+ const { key, proof } = clientOrConfig;
392
+ const Client = await import("@storacha/client");
393
+ const ed25519 = await import("@storacha/client/principal/ed25519");
394
+ const { StoreMemory } = await import("@storacha/client/stores/memory");
395
+ const Proof = await import("@storacha/client/proof");
396
+ const principal = ed25519.parse(key);
397
+ const client = await Client.create({ principal, store: new StoreMemory() });
398
+ const space = await client.addSpace(await Proof.parse(proof));
399
+ await client.setCurrentSpace(space.did());
400
+ resolvedClient = client;
401
+ return client;
402
+ })();
403
+ }
404
+ return initPromise;
405
+ }
406
+ return {
407
+ async uploadJson(data) {
408
+ const client = await getClient();
409
+ if (!client) throw new Error("Storacha client failed to initialize");
410
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
411
+ type: "application/json"
412
+ });
413
+ const cid = await client.uploadFile(blob);
414
+ return cid.toString();
415
+ },
416
+ async fetchJson(cid) {
417
+ const res = await fetch(`${DEFAULT_IPFS_GATEWAY}/${cid}`);
418
+ if (!res.ok) throw new Error(`IPFS fetch failed: ${res.statusText}`);
419
+ return res.json();
420
+ }
421
+ };
422
+ }
423
+ function createMemoryAdapter() {
424
+ const store = /* @__PURE__ */ new Map();
425
+ let counter = 0;
426
+ return {
427
+ async uploadJson(data) {
428
+ const json = JSON.stringify(data);
429
+ const cid = `mem_${++counter}_${Date.now()}`;
430
+ store.set(cid, json);
431
+ return cid;
432
+ },
433
+ async fetchJson(cid) {
434
+ const json = store.get(cid);
435
+ if (!json) throw new Error(`CID not found in memory store: ${cid}`);
436
+ return JSON.parse(json);
437
+ }
438
+ };
439
+ }
440
+ function buildPaymentReceipt(event, txHash, blockNumber, chainId) {
441
+ return {
442
+ subscriptionId: event.subscriptionId.toString(),
443
+ planId: event.planId.toString(),
444
+ subscriber: event.subscriber,
445
+ merchant: event.merchant,
446
+ amount: event.amount.toString(),
447
+ protocolFee: event.protocolFee.toString(),
448
+ timestamp: Number(event.timestamp),
449
+ txHash,
450
+ blockNumber,
451
+ chainId
452
+ };
453
+ }
454
+ function ipfsGatewayUrl(cid) {
455
+ return `${DEFAULT_IPFS_GATEWAY}/${cid}`;
456
+ }
457
+
458
+ // src/client.ts
459
+ var VeraPayClient = class _VeraPayClient {
460
+ contract;
461
+ provider;
462
+ config;
463
+ signer;
464
+ ipfs;
465
+ constructor(config2, signerOrProvider, ipfsAdapter) {
466
+ this.config = config2;
467
+ this.ipfs = ipfsAdapter;
468
+ if ("getAddress" in signerOrProvider) {
469
+ this.signer = signerOrProvider;
470
+ this.provider = signerOrProvider.provider;
471
+ } else {
472
+ this.provider = signerOrProvider;
473
+ }
474
+ this.contract = new import_ethers.ethers.Contract(
475
+ config2.contractAddress,
476
+ VERA_PAY_ABI,
477
+ this.signer ?? this.provider
478
+ );
479
+ }
480
+ /**
481
+ * Convenience factory that auto-configures for a known Flow network.
482
+ */
483
+ static fromNetwork(network, contractAddress, signerOrProvider, ipfsAdapter) {
484
+ const net = NETWORKS[network];
485
+ return new _VeraPayClient(
486
+ { contractAddress, rpcUrl: net.rpcUrl, chainId: net.chainId },
487
+ signerOrProvider,
488
+ ipfsAdapter
489
+ );
490
+ }
491
+ // ── Merchant API ─────────────────────────────────────────────────────
492
+ async createPlan(params) {
493
+ this.requireSigner();
494
+ const tx2 = await this.contract.createPlan(
495
+ params.paymentToken,
496
+ params.amount,
497
+ params.interval,
498
+ params.name,
499
+ params.metadataURI ?? ""
500
+ );
501
+ const receipt = await tx2.wait();
502
+ const log = receipt.logs.find(
503
+ (l) => l.topics[0] === import_ethers.ethers.id(
504
+ "PlanCreated(uint256,address,address,uint256,uint256,string,string)"
505
+ )
506
+ );
507
+ const planId = log ? BigInt(log.topics[1]) : 0n;
508
+ return { planId, tx: tx2 };
509
+ }
510
+ async togglePlan(planId) {
511
+ this.requireSigner();
512
+ return this.contract.togglePlan(planId);
513
+ }
514
+ async getMerchantPlans(merchant) {
515
+ return this.contract.getMerchantPlans(merchant);
516
+ }
517
+ // ── Subscriber API ───────────────────────────────────────────────────
518
+ /**
519
+ * Approve the VeraPay contract to pull `amount` of the given ERC-20 token,
520
+ * then subscribe to the plan. Returns subscription ID and the payment receipt
521
+ * (optionally pinned to IPFS).
522
+ */
523
+ async subscribeWithApproval(planId, options) {
524
+ this.requireSigner();
525
+ const plan = await this.getPlan(planId);
526
+ const token = new import_ethers.ethers.Contract(
527
+ plan.paymentToken,
528
+ ERC20_ABI,
529
+ this.signer
530
+ );
531
+ const approveAmount = options?.approveMax ? import_ethers.ethers.MaxUint256 : plan.amount * 12n;
532
+ const approveTx = await token.approve(
533
+ this.config.contractAddress,
534
+ approveAmount
535
+ );
536
+ await approveTx.wait();
537
+ return this.subscribe(planId);
538
+ }
539
+ async subscribe(planId) {
540
+ this.requireSigner();
541
+ const tx2 = await this.contract.subscribe(planId);
542
+ const txReceipt = await tx2.wait();
543
+ const paymentReceipt = await this.extractPaymentReceipt(txReceipt);
544
+ const subLog = txReceipt.logs.find(
545
+ (l) => l.topics[0] === import_ethers.ethers.id("Subscribed(uint256,uint256,address)")
546
+ );
547
+ const subscriptionId = subLog ? BigInt(subLog.topics[1]) : 0n;
548
+ return { subscriptionId, receipt: paymentReceipt, tx: tx2 };
549
+ }
550
+ async cancelSubscription(subscriptionId) {
551
+ this.requireSigner();
552
+ return this.contract.cancelSubscription(subscriptionId);
553
+ }
554
+ async getSubscriberSubscriptions(subscriber) {
555
+ return this.contract.getSubscriberSubscriptions(subscriber);
556
+ }
557
+ // ── Keeper / Relayer API ─────────────────────────────────────────────
558
+ async processPayment(subscriptionId) {
559
+ this.requireSigner();
560
+ const tx2 = await this.contract.processPayment(subscriptionId);
561
+ const txReceipt = await tx2.wait();
562
+ const receipt = await this.extractPaymentReceipt(txReceipt);
563
+ return { receipt, tx: tx2 };
564
+ }
565
+ async batchProcessPayments(subscriptionIds) {
566
+ this.requireSigner();
567
+ return this.contract.batchProcessPayments(subscriptionIds);
568
+ }
569
+ async isPaymentDue(subscriptionId) {
570
+ return this.contract.isPaymentDue(subscriptionId);
571
+ }
572
+ async getDuePayments(subscriptionIds) {
573
+ return this.contract.getDuePayments(subscriptionIds);
574
+ }
575
+ /**
576
+ * Start a polling loop that checks for due payments and processes them.
577
+ * Returns a cleanup function to stop the loop.
578
+ */
579
+ startKeeper(subscriptionIds, intervalMs = 6e4, onPayment, onError) {
580
+ let running = true;
581
+ const tick = async () => {
582
+ if (!running) return;
583
+ try {
584
+ const due = await this.getDuePayments(subscriptionIds);
585
+ for (const subId of due) {
586
+ const { receipt } = await this.processPayment(subId);
587
+ onPayment?.(receipt);
588
+ }
589
+ } catch (err) {
590
+ onError?.(err);
591
+ }
592
+ if (running) setTimeout(tick, intervalMs);
593
+ };
594
+ tick();
595
+ return () => {
596
+ running = false;
597
+ };
598
+ }
599
+ // ── Read helpers ─────────────────────────────────────────────────────
600
+ async getNextPlanId() {
601
+ return this.contract.nextPlanId();
602
+ }
603
+ async getNextSubscriptionId() {
604
+ return this.contract.nextSubscriptionId();
605
+ }
606
+ async listActivePlans() {
607
+ const nextId = await this.getNextPlanId();
608
+ const plans = [];
609
+ for (let i = 0n; i < nextId; i++) {
610
+ try {
611
+ const plan = await this.getPlan(i);
612
+ if (plan.active) plans.push(plan);
613
+ } catch {
614
+ }
615
+ }
616
+ return plans;
617
+ }
618
+ async getPlan(planId) {
619
+ const raw = await this.contract.getPlan(planId);
620
+ return {
621
+ planId,
622
+ merchant: raw.merchant,
623
+ paymentToken: raw.paymentToken,
624
+ amount: raw.amount,
625
+ interval: raw.interval,
626
+ name: raw.name,
627
+ metadataURI: raw.metadataURI,
628
+ active: raw.active
629
+ };
630
+ }
631
+ async getSubscription(subscriptionId) {
632
+ const raw = await this.contract.getSubscription(subscriptionId);
633
+ return {
634
+ subscriptionId,
635
+ planId: raw.planId,
636
+ subscriber: raw.subscriber,
637
+ startTime: raw.startTime,
638
+ lastPaymentTime: raw.lastPaymentTime,
639
+ paymentsCount: raw.paymentsCount,
640
+ active: raw.active
641
+ };
642
+ }
643
+ async getProtocolFeeBps() {
644
+ return this.contract.protocolFeeBps();
645
+ }
646
+ // ── IPFS ─────────────────────────────────────────────────────────────
647
+ get hasIPFS() {
648
+ return !!this.ipfs;
649
+ }
650
+ get contractAddress() {
651
+ return this.config.contractAddress;
652
+ }
653
+ /**
654
+ * Pin a payment receipt to IPFS and return the CID.
655
+ */
656
+ async pinReceipt(receipt) {
657
+ if (!this.ipfs) throw new Error("No IPFS adapter configured");
658
+ const cid = await this.ipfs.uploadJson(receipt);
659
+ receipt.ipfsCid = cid;
660
+ return cid;
661
+ }
662
+ async fetchReceipt(cid) {
663
+ if (!this.ipfs) throw new Error("No IPFS adapter configured");
664
+ return this.ipfs.fetchJson(cid);
665
+ }
666
+ setIPFSAdapter(adapter) {
667
+ this.ipfs = adapter;
668
+ }
669
+ // ── Listeners ────────────────────────────────────────────────────────
670
+ onPaymentProcessed(callback, filter) {
671
+ const eventFilter = this.contract.filters.PaymentProcessed(
672
+ filter?.subscriptionId ?? null,
673
+ filter?.planId ?? null,
674
+ filter?.subscriber ?? null
675
+ );
676
+ this.contract.on(
677
+ eventFilter,
678
+ async (subscriptionId, planId, subscriber, merchant, amount, protocolFee, timestamp, event) => {
679
+ const receipt = buildPaymentReceipt(
680
+ { subscriptionId, planId, subscriber, merchant, amount, protocolFee, timestamp },
681
+ event.transactionHash,
682
+ event.blockNumber,
683
+ this.config.chainId ?? 0
684
+ );
685
+ if (this.ipfs) {
686
+ try {
687
+ receipt.ipfsCid = await this.ipfs.uploadJson(receipt);
688
+ } catch {
689
+ }
690
+ }
691
+ callback(receipt);
692
+ }
693
+ );
694
+ return this.contract;
695
+ }
696
+ removeAllListeners() {
697
+ this.contract.removeAllListeners();
698
+ }
699
+ // ── Internal ─────────────────────────────────────────────────────────
700
+ requireSigner() {
701
+ if (!this.signer) {
702
+ throw new Error("A signer is required for write operations");
703
+ }
704
+ }
705
+ async extractPaymentReceipt(txReceipt) {
706
+ const iface = new import_ethers.ethers.Interface(VERA_PAY_ABI);
707
+ const paymentTopic = import_ethers.ethers.id(
708
+ "PaymentProcessed(uint256,uint256,address,address,uint256,uint256,uint256)"
709
+ );
710
+ for (const log of txReceipt.logs) {
711
+ if (log.topics[0] === paymentTopic) {
712
+ const parsed = iface.parseLog({
713
+ topics: [...log.topics],
714
+ data: log.data
715
+ });
716
+ if (!parsed) continue;
717
+ const receipt = buildPaymentReceipt(
718
+ {
719
+ subscriptionId: BigInt(log.topics[1]),
720
+ planId: BigInt(log.topics[2]),
721
+ subscriber: import_ethers.ethers.getAddress("0x" + log.topics[3].slice(26)),
722
+ merchant: parsed.args.merchant,
723
+ amount: parsed.args.amount,
724
+ protocolFee: parsed.args.protocolFee,
725
+ timestamp: parsed.args.timestamp
726
+ },
727
+ txReceipt.hash,
728
+ txReceipt.blockNumber,
729
+ this.config.chainId ?? 0
730
+ );
731
+ if (this.ipfs) {
732
+ try {
733
+ receipt.ipfsCid = await this.ipfs.uploadJson(receipt);
734
+ } catch {
735
+ }
736
+ }
737
+ return receipt;
738
+ }
739
+ }
740
+ return {
741
+ subscriptionId: "0",
742
+ planId: "0",
743
+ subscriber: "",
744
+ merchant: "",
745
+ amount: "0",
746
+ protocolFee: "0",
747
+ timestamp: 0,
748
+ txHash: txReceipt.hash,
749
+ blockNumber: txReceipt.blockNumber,
750
+ chainId: this.config.chainId ?? 0
751
+ };
752
+ }
753
+ };
754
+
755
+ // src/scheduler.ts
756
+ var fcl = __toESM(require("@onflow/fcl"));
757
+ var NETWORK_CONFIG = {
758
+ testnet: {
759
+ accessNode: "https://rest-testnet.onflow.org",
760
+ discoveryWallet: "https://fcl-discovery.onflow.org/testnet/authn",
761
+ flowNetwork: "testnet",
762
+ contracts: {
763
+ FlowTransactionScheduler: "0x8c5303eaa26202d6",
764
+ FlowTransactionSchedulerUtils: "0x8c5303eaa26202d6",
765
+ FlowToken: "0x7e60df042a9c0868",
766
+ FungibleToken: "0x9a0766d93b6608b7",
767
+ EVM: "0x8c5303eaa26202d6"
768
+ }
769
+ },
770
+ mainnet: {
771
+ accessNode: "https://rest-mainnet.onflow.org",
772
+ discoveryWallet: "https://fcl-discovery.onflow.org/authn",
773
+ flowNetwork: "mainnet",
774
+ contracts: {
775
+ FlowTransactionScheduler: "0xe467b9dd11fa00df",
776
+ FlowTransactionSchedulerUtils: "0xe467b9dd11fa00df",
777
+ FlowToken: "0x1654653399040a61",
778
+ FungibleToken: "0xf233dcee88fe0abe",
779
+ EVM: "0xe467b9dd11fa00df"
780
+ }
781
+ }
782
+ };
783
+ function schedulePaymentCdc(c) {
784
+ return `
785
+ import FlowTransactionScheduler from ${c.FlowTransactionScheduler}
786
+ import FlowTransactionSchedulerUtils from ${c.FlowTransactionSchedulerUtils}
787
+ import FlowToken from ${c.FlowToken}
788
+ import FungibleToken from ${c.FungibleToken}
789
+
790
+ transaction(
791
+ subscriptionId: UInt256,
792
+ delaySeconds: UFix64,
793
+ priority: UInt8,
794
+ executionEffort: UInt64
795
+ ) {
796
+ prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, GetStorageCapabilityController) &Account) {
797
+ let future = getCurrentBlock().timestamp + delaySeconds
798
+
799
+ let pr = priority == 0
800
+ ? FlowTransactionScheduler.Priority.High
801
+ : priority == 1
802
+ ? FlowTransactionScheduler.Priority.Medium
803
+ : FlowTransactionScheduler.Priority.Low
804
+
805
+ var handlerCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>? = nil
806
+ let controllers = signer.capabilities.storage.getControllers(forPath: /storage/VeraPayScheduledPaymentHandler)
807
+ for controller in controllers {
808
+ if let cap = controller.capability as? Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}> {
809
+ handlerCap = cap
810
+ break
811
+ }
812
+ }
813
+
814
+ assert(handlerCap != nil, message: "No handler capability found. Run InitVeraPayHandler first.")
815
+
816
+ if signer.storage.borrow<&AnyResource>(from: FlowTransactionSchedulerUtils.managerStoragePath) == nil {
817
+ let manager <- FlowTransactionSchedulerUtils.createManager()
818
+ signer.storage.save(<-manager, to: FlowTransactionSchedulerUtils.managerStoragePath)
819
+
820
+ let managerCap = signer.capabilities.storage.issue<&{FlowTransactionSchedulerUtils.Manager}>(
821
+ FlowTransactionSchedulerUtils.managerStoragePath
822
+ )
823
+ signer.capabilities.publish(managerCap, at: FlowTransactionSchedulerUtils.managerPublicPath)
824
+ }
825
+
826
+ let manager = signer.storage.borrow<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>(
827
+ from: FlowTransactionSchedulerUtils.managerStoragePath
828
+ ) ?? panic("Could not borrow Manager")
829
+
830
+ let est = FlowTransactionScheduler.estimate(
831
+ data: subscriptionId as AnyStruct,
832
+ timestamp: future,
833
+ priority: pr,
834
+ executionEffort: executionEffort
835
+ )
836
+
837
+ assert(
838
+ est.timestamp != nil || pr == FlowTransactionScheduler.Priority.Low,
839
+ message: est.error ?? "Fee estimation failed"
840
+ )
841
+
842
+ let vault = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
843
+ from: /storage/flowTokenVault
844
+ ) ?? panic("Could not borrow FlowToken vault")
845
+
846
+ let fees <- vault.withdraw(amount: est.flowFee ?? 0.0) as! @FlowToken.Vault
847
+
848
+ let transactionId = manager.schedule(
849
+ handlerCap: handlerCap!,
850
+ data: subscriptionId,
851
+ timestamp: future,
852
+ priority: pr,
853
+ executionEffort: executionEffort,
854
+ fees: <-fees
855
+ )
856
+
857
+ log("Scheduled payment for sub #".concat(subscriptionId.toString())
858
+ .concat(" | txId: ").concat(transactionId.toString())
859
+ .concat(" | at: ").concat(future.toString()))
860
+ }
861
+ }
862
+ `;
863
+ }
864
+ function cancelPaymentCdc(c) {
865
+ return `
866
+ import FlowTransactionScheduler from ${c.FlowTransactionScheduler}
867
+ import FlowTransactionSchedulerUtils from ${c.FlowTransactionSchedulerUtils}
868
+ import FlowToken from ${c.FlowToken}
869
+ import FungibleToken from ${c.FungibleToken}
870
+
871
+ transaction(transactionId: UInt64) {
872
+ prepare(signer: auth(BorrowValue) &Account) {
873
+ let manager = signer.storage.borrow<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>(
874
+ from: FlowTransactionSchedulerUtils.managerStoragePath
875
+ ) ?? panic("Could not borrow Manager")
876
+
877
+ let vault = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
878
+ from: /storage/flowTokenVault
879
+ ) ?? panic("Could not borrow FlowToken vault")
880
+
881
+ let refund <- manager.cancel(id: transactionId)
882
+ vault.deposit(from: <-refund)
883
+
884
+ log("Cancelled scheduled transaction #".concat(transactionId.toString()))
885
+ }
886
+ }
887
+ `;
888
+ }
889
+ function setupCoaCdc(c) {
890
+ return `
891
+ import EVM from ${c.EVM}
892
+ import FlowToken from ${c.FlowToken}
893
+ import FungibleToken from ${c.FungibleToken}
894
+
895
+ transaction(fundAmount: UFix64) {
896
+ prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability) &Account) {
897
+ if signer.storage.borrow<&EVM.CadenceOwnedAccount>(from: /storage/evm) != nil {
898
+ log("COA already exists at /storage/evm")
899
+ return
900
+ }
901
+
902
+ let coa <- EVM.createCadenceOwnedAccount()
903
+
904
+ if fundAmount > 0.0 {
905
+ let vault = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
906
+ from: /storage/flowTokenVault
907
+ ) ?? panic("Could not borrow FlowToken vault")
908
+
909
+ let tokens <- vault.withdraw(amount: fundAmount) as! @FlowToken.Vault
910
+ coa.deposit(from: <-tokens)
911
+ }
912
+
913
+ signer.storage.save(<-coa, to: /storage/evm)
914
+
915
+ let callCap = signer.capabilities.storage.issue<auth(EVM.Call) &EVM.CadenceOwnedAccount>(/storage/evm)
916
+ let publicCap = signer.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(/storage/evm)
917
+ signer.capabilities.publish(publicCap, at: /public/evm)
918
+
919
+ log("COA created and funded with ".concat(fundAmount.toString()).concat(" FLOW"))
920
+ }
921
+ }
922
+ `;
923
+ }
924
+ function initHandlerCdc(c, handlerAddr) {
925
+ return `
926
+ import VeraPayScheduledPaymentHandler from 0x${handlerAddr}
927
+ import FlowTransactionScheduler from ${c.FlowTransactionScheduler}
928
+ import EVM from ${c.EVM}
929
+
930
+ transaction(verapayEvmAddress: String) {
931
+ prepare(signer: auth(BorrowValue, SaveValue, LoadValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability, GetStorageCapabilityController) &Account) {
932
+ // 1. Destroy old handler resource if it exists
933
+ if let oldHandler <- signer.storage.load<@AnyResource>(from: /storage/VeraPayScheduledPaymentHandler) {
934
+ destroy oldHandler
935
+ log("Destroyed old VeraPay handler")
936
+ }
937
+
938
+ // 2. Unpublish old public capability
939
+ signer.capabilities.unpublish(/public/VeraPayScheduledPaymentHandler)
940
+
941
+ // 3. Delete all old capability controllers for the handler storage path
942
+ let oldControllers = signer.capabilities.storage.getControllers(forPath: /storage/VeraPayScheduledPaymentHandler)
943
+ for controller in oldControllers {
944
+ controller.delete()
945
+ }
946
+
947
+ // 4. Get or create COA capability
948
+ let evmAddr = EVM.addressFromString(verapayEvmAddress)
949
+
950
+ var coaCap: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>? = nil
951
+ let controllers = signer.capabilities.storage.getControllers(forPath: /storage/evm)
952
+ for controller in controllers {
953
+ if let cap = controller.capability as? Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount> {
954
+ coaCap = cap
955
+ break
956
+ }
957
+ }
958
+
959
+ if coaCap == nil {
960
+ coaCap = signer.capabilities.storage.issue<auth(EVM.Call) &EVM.CadenceOwnedAccount>(/storage/evm)
961
+ }
962
+
963
+ // 5. Create fresh handler with new address
964
+ let handler <- VeraPayScheduledPaymentHandler.createHandler(
965
+ coaCapability: coaCap!,
966
+ verapayAddress: evmAddr
967
+ )
968
+ signer.storage.save(<-handler, to: /storage/VeraPayScheduledPaymentHandler)
969
+
970
+ // 6. Issue fresh capabilities
971
+ signer.capabilities.storage.issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(
972
+ /storage/VeraPayScheduledPaymentHandler
973
+ )
974
+
975
+ let publicCap = signer.capabilities.storage.issue<&{FlowTransactionScheduler.TransactionHandler}>(
976
+ /storage/VeraPayScheduledPaymentHandler
977
+ )
978
+ signer.capabilities.publish(publicCap, at: /public/VeraPayScheduledPaymentHandler)
979
+
980
+ log("VeraPay handler initialized for contract: ".concat(verapayEvmAddress))
981
+ }
982
+ }
983
+ `;
984
+ }
985
+ var FlowScheduler = class _FlowScheduler {
986
+ constructor(config2) {
987
+ this.config = config2;
988
+ this.ipfs = config2.ipfsAdapter;
989
+ }
990
+ configured = false;
991
+ ipfs;
992
+ get hasIPFS() {
993
+ return !!this.ipfs;
994
+ }
995
+ setIPFSAdapter(adapter) {
996
+ this.ipfs = adapter;
997
+ }
998
+ ensureConfigured() {
999
+ if (this.configured) return;
1000
+ const net = NETWORK_CONFIG[this.config.network];
1001
+ fcl.config().put("accessNode.api", net.accessNode).put("discovery.wallet", net.discoveryWallet).put("flow.network", net.flowNetwork);
1002
+ this.configured = true;
1003
+ }
1004
+ /** Trigger FCL wallet authentication (Blocto, Lilico, etc.) */
1005
+ async authenticate() {
1006
+ this.ensureConfigured();
1007
+ const user = await fcl.authenticate();
1008
+ return { addr: user.addr ?? "" };
1009
+ }
1010
+ /** Disconnect the current wallet */
1011
+ async unauthenticate() {
1012
+ await fcl.unauthenticate();
1013
+ }
1014
+ /** Get the currently authenticated Flow address, or null */
1015
+ async currentUser() {
1016
+ this.ensureConfigured();
1017
+ const snapshot = await fcl.currentUser.snapshot();
1018
+ return snapshot.addr ?? null;
1019
+ }
1020
+ /**
1021
+ * Create a Cadence Owned Account (COA) and fund it with FLOW for EVM gas.
1022
+ * Idempotent — skips if COA already exists. Must be called before initHandler.
1023
+ * Returns the Flow transaction ID.
1024
+ */
1025
+ async setupCOA(fundAmount = "1.0") {
1026
+ this.ensureConfigured();
1027
+ const contracts = NETWORK_CONFIG[this.config.network].contracts;
1028
+ const txId = await fcl.mutate({
1029
+ cadence: setupCoaCdc(contracts),
1030
+ args: (arg, t) => [arg(fundAmount, t.UFix64)],
1031
+ limit: 9999
1032
+ });
1033
+ return txId;
1034
+ }
1035
+ /**
1036
+ * Initialize the VeraPay scheduled payment handler on the connected account.
1037
+ * Idempotent — skips if already initialized. Requires a COA (call setupCOA first).
1038
+ * Returns the Flow transaction ID.
1039
+ */
1040
+ async initHandler() {
1041
+ this.ensureConfigured();
1042
+ const contracts = NETWORK_CONFIG[this.config.network].contracts;
1043
+ const evmAddr = this.config.evmContractAddress.startsWith("0x") ? this.config.evmContractAddress : "0x" + this.config.evmContractAddress;
1044
+ const txId = await fcl.mutate({
1045
+ cadence: initHandlerCdc(contracts, this.config.handlerAddress),
1046
+ args: (arg, t) => [arg(evmAddr, t.String)],
1047
+ limit: 9999
1048
+ });
1049
+ return txId;
1050
+ }
1051
+ /**
1052
+ * Convenience method: runs setupCOA + initHandler in sequence.
1053
+ * Both are idempotent so it's safe to call on every session.
1054
+ */
1055
+ async setup(fundAmount = "1.0") {
1056
+ const coaTxId = await this.setupCOA(fundAmount);
1057
+ await this.waitForTransaction(coaTxId);
1058
+ const handlerTxId = await this.initHandler();
1059
+ await this.waitForTransaction(handlerTxId);
1060
+ return { coaTxId, handlerTxId };
1061
+ }
1062
+ /**
1063
+ * Schedule a subscription payment to be executed at a future time.
1064
+ * The connected Flow wallet pays the scheduling fees.
1065
+ * Returns the Flow transaction ID.
1066
+ */
1067
+ async schedulePayment(params) {
1068
+ this.ensureConfigured();
1069
+ const contracts = NETWORK_CONFIG[this.config.network].contracts;
1070
+ const txId = await fcl.mutate({
1071
+ cadence: schedulePaymentCdc(contracts),
1072
+ args: (arg, t) => [
1073
+ arg(params.subscriptionId, t.UInt256),
1074
+ arg(params.delaySeconds, t.UFix64),
1075
+ arg(String(params.priority), t.UInt8),
1076
+ arg(String(params.executionEffort), t.UInt64)
1077
+ ],
1078
+ limit: 9999
1079
+ });
1080
+ return txId;
1081
+ }
1082
+ /**
1083
+ * Cancel a previously scheduled payment and receive a partial fee refund.
1084
+ * Returns the Flow transaction ID.
1085
+ */
1086
+ async cancelScheduledPayment(scheduledTransactionId) {
1087
+ this.ensureConfigured();
1088
+ const contracts = NETWORK_CONFIG[this.config.network].contracts;
1089
+ const txId = await fcl.mutate({
1090
+ cadence: cancelPaymentCdc(contracts),
1091
+ args: (arg, t) => [
1092
+ arg(scheduledTransactionId, t.UInt64)
1093
+ ],
1094
+ limit: 9999
1095
+ });
1096
+ return txId;
1097
+ }
1098
+ /**
1099
+ * Make the COA approve an EVM spender (e.g., VeraPay) to transfer ERC20 tokens.
1100
+ * This is required before the scheduled payment can pull tokens from the COA.
1101
+ * Returns the Flow transaction ID.
1102
+ */
1103
+ async approveERC20(tokenAddress, amount = "115792089237316195423570985008687907853269984665640564039457584007913129639935") {
1104
+ this.ensureConfigured();
1105
+ const contracts = NETWORK_CONFIG[this.config.network].contracts;
1106
+ const token = tokenAddress.startsWith("0x") ? tokenAddress : "0x" + tokenAddress;
1107
+ const spender = this.config.evmContractAddress.startsWith("0x") ? this.config.evmContractAddress : "0x" + this.config.evmContractAddress;
1108
+ const txId = await fcl.mutate({
1109
+ cadence: `
1110
+ import EVM from ${contracts.EVM}
1111
+
1112
+ transaction(tokenAddress: String, spenderAddress: String, amount: UInt256) {
1113
+ prepare(signer: auth(BorrowValue) &Account) {
1114
+ let coa = signer.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm)
1115
+ ?? panic("No COA found. Run Setup first.")
1116
+
1117
+ let calldata = EVM.encodeABIWithSignature(
1118
+ "approve(address,uint256)",
1119
+ [EVM.addressFromString(spenderAddress), amount]
1120
+ )
1121
+
1122
+ let result = coa.call(
1123
+ to: EVM.addressFromString(tokenAddress),
1124
+ data: calldata,
1125
+ gasLimit: 100_000,
1126
+ value: EVM.Balance(attoflow: 0)
1127
+ )
1128
+
1129
+ assert(result.status == EVM.Status.successful, message: "ERC20 approve failed")
1130
+ }
1131
+ }
1132
+ `,
1133
+ args: (arg, t) => [
1134
+ arg(token, t.String),
1135
+ arg(spender, t.String),
1136
+ arg(amount, t.UInt256)
1137
+ ],
1138
+ limit: 9999
1139
+ });
1140
+ return txId;
1141
+ }
1142
+ /**
1143
+ * Subscribe the COA to a VeraPay plan AND schedule the next recurring payment
1144
+ * in a single Cadence transaction.
1145
+ *
1146
+ * 1. COA calls subscribe(planId) on the EVM contract (first payment is charged immediately)
1147
+ * 2. Decodes the returned subscription ID from the EVM call
1148
+ * 3. Schedules the next payment via FlowTransactionScheduler with the plan's interval as delay
1149
+ *
1150
+ * If an IPFS adapter is configured, the payment receipt is automatically
1151
+ * pinned after the transaction seals.
1152
+ *
1153
+ * Returns the Flow tx ID, and optionally the receipt + IPFS CID.
1154
+ */
1155
+ async subscribeAndSchedule(params) {
1156
+ this.ensureConfigured();
1157
+ const contracts = NETWORK_CONFIG[this.config.network].contracts;
1158
+ const verapay = this.config.evmContractAddress.startsWith("0x") ? this.config.evmContractAddress : "0x" + this.config.evmContractAddress;
1159
+ const txId = await fcl.mutate({
1160
+ cadence: `
1161
+ import EVM from ${contracts.EVM}
1162
+ import FlowTransactionScheduler from ${contracts.FlowTransactionScheduler}
1163
+ import FlowTransactionSchedulerUtils from ${contracts.FlowTransactionSchedulerUtils}
1164
+ import FlowToken from ${contracts.FlowToken}
1165
+ import FungibleToken from ${contracts.FungibleToken}
1166
+
1167
+ transaction(
1168
+ verapayAddress: String,
1169
+ planId: UInt256,
1170
+ intervalSeconds: UFix64,
1171
+ priority: UInt8,
1172
+ executionEffort: UInt64
1173
+ ) {
1174
+ prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, GetStorageCapabilityController) &Account) {
1175
+ // 1. Subscribe via COA
1176
+ let coa = signer.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm)
1177
+ ?? panic("No COA found. Run Setup first.")
1178
+
1179
+ let calldata = EVM.encodeABIWithSignature(
1180
+ "subscribe(uint256)",
1181
+ [planId]
1182
+ )
1183
+
1184
+ let result = coa.call(
1185
+ to: EVM.addressFromString(verapayAddress),
1186
+ data: calldata,
1187
+ gasLimit: 300_000,
1188
+ value: EVM.Balance(attoflow: 0)
1189
+ )
1190
+
1191
+ assert(result.status == EVM.Status.successful,
1192
+ message: "VeraPay subscribe failed: ".concat(result.errorCode.toString()))
1193
+
1194
+ // 2. Decode the returned subscription ID (uint256)
1195
+ let decoded = EVM.decodeABI(types: [Type<UInt256>()], data: result.data)
1196
+ let subscriptionId = decoded[0] as! UInt256
1197
+
1198
+ log("Subscribed! Sub ID: ".concat(subscriptionId.toString()))
1199
+
1200
+ // 3. Schedule next payment
1201
+ let future = getCurrentBlock().timestamp + intervalSeconds
1202
+
1203
+ let pr = priority == 0
1204
+ ? FlowTransactionScheduler.Priority.High
1205
+ : priority == 1
1206
+ ? FlowTransactionScheduler.Priority.Medium
1207
+ : FlowTransactionScheduler.Priority.Low
1208
+
1209
+ var handlerCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>? = nil
1210
+ let controllers = signer.capabilities.storage.getControllers(forPath: /storage/VeraPayScheduledPaymentHandler)
1211
+ for controller in controllers {
1212
+ if let cap = controller.capability as? Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}> {
1213
+ handlerCap = cap
1214
+ break
1215
+ }
1216
+ }
1217
+
1218
+ assert(handlerCap != nil, message: "No handler capability found. Run InitVeraPayHandler first.")
1219
+
1220
+ if signer.storage.borrow<&AnyResource>(from: FlowTransactionSchedulerUtils.managerStoragePath) == nil {
1221
+ let manager <- FlowTransactionSchedulerUtils.createManager()
1222
+ signer.storage.save(<-manager, to: FlowTransactionSchedulerUtils.managerStoragePath)
1223
+
1224
+ let managerCap = signer.capabilities.storage.issue<&{FlowTransactionSchedulerUtils.Manager}>(
1225
+ FlowTransactionSchedulerUtils.managerStoragePath
1226
+ )
1227
+ signer.capabilities.publish(managerCap, at: FlowTransactionSchedulerUtils.managerPublicPath)
1228
+ }
1229
+
1230
+ let manager = signer.storage.borrow<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>(
1231
+ from: FlowTransactionSchedulerUtils.managerStoragePath
1232
+ ) ?? panic("Could not borrow Manager")
1233
+
1234
+ let est = FlowTransactionScheduler.estimate(
1235
+ data: subscriptionId as AnyStruct,
1236
+ timestamp: future,
1237
+ priority: pr,
1238
+ executionEffort: executionEffort
1239
+ )
1240
+
1241
+ assert(
1242
+ est.timestamp != nil || pr == FlowTransactionScheduler.Priority.Low,
1243
+ message: est.error ?? "Fee estimation failed"
1244
+ )
1245
+
1246
+ let vault = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
1247
+ from: /storage/flowTokenVault
1248
+ ) ?? panic("Could not borrow FlowToken vault")
1249
+
1250
+ let fees <- vault.withdraw(amount: est.flowFee ?? 0.0) as! @FlowToken.Vault
1251
+
1252
+ let transactionId = manager.schedule(
1253
+ handlerCap: handlerCap!,
1254
+ data: subscriptionId,
1255
+ timestamp: future,
1256
+ priority: pr,
1257
+ executionEffort: executionEffort,
1258
+ fees: <-fees
1259
+ )
1260
+
1261
+ log("Scheduled next payment for sub #".concat(subscriptionId.toString())
1262
+ .concat(" | txId: ").concat(transactionId.toString())
1263
+ .concat(" | at: ").concat(future.toString()))
1264
+ }
1265
+ }
1266
+ `,
1267
+ args: (arg, t) => [
1268
+ arg(verapay, t.String),
1269
+ arg(String(params.planId), t.UInt256),
1270
+ arg(params.intervalSeconds, t.UFix64),
1271
+ arg(String(params.priority ?? 1), t.UInt8),
1272
+ arg(String(params.executionEffort ?? 1e3), t.UInt64)
1273
+ ],
1274
+ limit: 9999
1275
+ });
1276
+ const result = { flowTxId: txId };
1277
+ try {
1278
+ const sealed = await this.waitForTransaction(txId);
1279
+ result.scheduledTxId = _FlowScheduler.extractScheduledTxId(sealed.events);
1280
+ if (this.ipfs) {
1281
+ try {
1282
+ const receipt = {
1283
+ subscriptionId: "pending",
1284
+ planId: String(params.planId),
1285
+ subscriber: await this.currentUser() ?? "",
1286
+ merchant: "",
1287
+ amount: "0",
1288
+ protocolFee: "0",
1289
+ timestamp: Math.floor(Date.now() / 1e3),
1290
+ txHash: txId,
1291
+ blockNumber: 0,
1292
+ chainId: this.config.network === "testnet" ? 545 : 747
1293
+ };
1294
+ const cid = await this.ipfs.uploadJson(receipt);
1295
+ receipt.ipfsCid = cid;
1296
+ result.receipt = receipt;
1297
+ result.ipfsCid = cid;
1298
+ } catch {
1299
+ }
1300
+ }
1301
+ } catch {
1302
+ }
1303
+ return result;
1304
+ }
1305
+ /**
1306
+ * Subscribe the COA to a VeraPay plan via an EVM call (without scheduling).
1307
+ * The COA must have already approved the VeraPay contract to spend its tokens.
1308
+ * Returns the Flow transaction ID.
1309
+ */
1310
+ async subscribeToPlan(planId) {
1311
+ this.ensureConfigured();
1312
+ const contracts = NETWORK_CONFIG[this.config.network].contracts;
1313
+ const verapay = this.config.evmContractAddress.startsWith("0x") ? this.config.evmContractAddress : "0x" + this.config.evmContractAddress;
1314
+ const txId = await fcl.mutate({
1315
+ cadence: `
1316
+ import EVM from ${contracts.EVM}
1317
+
1318
+ transaction(verapayAddress: String, planId: UInt256) {
1319
+ prepare(signer: auth(BorrowValue) &Account) {
1320
+ let coa = signer.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm)
1321
+ ?? panic("No COA found. Run Setup first.")
1322
+
1323
+ let calldata = EVM.encodeABIWithSignature(
1324
+ "subscribe(uint256)",
1325
+ [planId]
1326
+ )
1327
+
1328
+ let result = coa.call(
1329
+ to: EVM.addressFromString(verapayAddress),
1330
+ data: calldata,
1331
+ gasLimit: 300_000,
1332
+ value: EVM.Balance(attoflow: 0)
1333
+ )
1334
+
1335
+ assert(result.status == EVM.Status.successful, message: "VeraPay subscribe failed: ".concat(result.errorCode.toString()))
1336
+ }
1337
+ }
1338
+ `,
1339
+ args: (arg, t) => [
1340
+ arg(verapay, t.String),
1341
+ arg(String(planId), t.UInt256)
1342
+ ],
1343
+ limit: 9999
1344
+ });
1345
+ return txId;
1346
+ }
1347
+ /**
1348
+ * Query the COA's EVM address for a given Flow account.
1349
+ * Returns the hex EVM address (0x...) or null if no COA exists.
1350
+ */
1351
+ async getCoaEvmAddress(flowAddress) {
1352
+ this.ensureConfigured();
1353
+ const addr = flowAddress ?? await this.currentUser();
1354
+ if (!addr) return null;
1355
+ const contracts = NETWORK_CONFIG[this.config.network].contracts;
1356
+ try {
1357
+ const result = await fcl.query({
1358
+ cadence: `
1359
+ import EVM from ${contracts.EVM}
1360
+
1361
+ access(all) fun main(flowAddress: Address): String? {
1362
+ if let acc = getAuthAccount<auth(BorrowValue) &Account>(flowAddress)
1363
+ .storage.borrow<&EVM.CadenceOwnedAccount>(from: /storage/evm) {
1364
+ return "0x".concat(acc.address().toString())
1365
+ }
1366
+ return nil
1367
+ }
1368
+ `,
1369
+ args: (arg, t) => [arg(addr, t.Address)]
1370
+ });
1371
+ return result;
1372
+ } catch {
1373
+ return null;
1374
+ }
1375
+ }
1376
+ /** Wait for a Flow transaction to be sealed. Returns the transaction result with events. */
1377
+ async waitForTransaction(txId) {
1378
+ this.ensureConfigured();
1379
+ const result = await fcl.tx(txId).onceSealed();
1380
+ return result;
1381
+ }
1382
+ /**
1383
+ * Extract the scheduled transaction ID from sealed tx events.
1384
+ * Looks for the FlowTransactionScheduler.TransactionScheduled event.
1385
+ */
1386
+ static extractScheduledTxId(events) {
1387
+ const evt = events.find((e) => e.type.includes("FlowTransactionScheduler") && e.type.includes("TransactionScheduled"));
1388
+ if (!evt) return void 0;
1389
+ const id = evt.data.id ?? evt.data.transactionId ?? evt.data.scheduledTransactionId;
1390
+ return id != null ? String(id) : void 0;
1391
+ }
1392
+ };
1393
+ // Annotate the CommonJS export names for ESM import in node:
1394
+ 0 && (module.exports = {
1395
+ CADENCE_HANDLERS,
1396
+ DEFAULT_IPFS_GATEWAY,
1397
+ DEPLOYED_CONTRACTS,
1398
+ ERC20_ABI,
1399
+ FlowScheduler,
1400
+ KNOWN_TOKENS,
1401
+ NETWORKS,
1402
+ VERA_PAY_ABI,
1403
+ VeraPayClient,
1404
+ buildPaymentReceipt,
1405
+ createKuboAdapter,
1406
+ createMemoryAdapter,
1407
+ createStorachaAdapter,
1408
+ ipfsGatewayUrl
1409
+ });