@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/README.md +388 -0
- package/dist/index.d.mts +772 -0
- package/dist/index.d.ts +772 -0
- package/dist/index.js +1409 -0
- package/dist/index.mjs +1359 -0
- package/package.json +50 -0
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
|
+
});
|