agentspay 0.1.0 → 0.2.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/.env.example +40 -0
- package/README.md +35 -4
- package/VERIFICATION_SUMMARY.txt +151 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +530 -74
- package/dist/api/server.js.map +1 -1
- package/dist/bsv/crypto.d.ts +65 -0
- package/dist/bsv/crypto.d.ts.map +1 -0
- package/dist/bsv/crypto.js +158 -0
- package/dist/bsv/crypto.js.map +1 -0
- package/dist/bsv/mnee.d.ts +88 -0
- package/dist/bsv/mnee.d.ts.map +1 -0
- package/dist/bsv/mnee.js +173 -0
- package/dist/bsv/mnee.js.map +1 -0
- package/dist/bsv/opreturn.d.ts +22 -0
- package/dist/bsv/opreturn.d.ts.map +1 -0
- package/dist/bsv/opreturn.js +117 -0
- package/dist/bsv/opreturn.js.map +1 -0
- package/dist/bsv/whatsonchain.d.ts +46 -0
- package/dist/bsv/whatsonchain.d.ts.map +1 -0
- package/dist/bsv/whatsonchain.js +98 -0
- package/dist/bsv/whatsonchain.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/currency/currency.d.ts +70 -0
- package/dist/currency/currency.d.ts.map +1 -0
- package/dist/currency/currency.js +137 -0
- package/dist/currency/currency.js.map +1 -0
- package/dist/disputes/dispute.d.ts +50 -0
- package/dist/disputes/dispute.d.ts.map +1 -0
- package/dist/disputes/dispute.js +162 -0
- package/dist/disputes/dispute.js.map +1 -0
- package/dist/docs/openapi.yaml +1079 -0
- package/dist/docs/swagger.d.ts +12 -0
- package/dist/docs/swagger.d.ts.map +1 -0
- package/dist/docs/swagger.js +104 -0
- package/dist/docs/swagger.js.map +1 -0
- package/dist/middleware/auth.d.ts +20 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +32 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/rateLimit.d.ts +25 -0
- package/dist/middleware/rateLimit.d.ts.map +1 -0
- package/dist/middleware/rateLimit.js +61 -0
- package/dist/middleware/rateLimit.js.map +1 -0
- package/dist/payment/payment.d.ts +79 -9
- package/dist/payment/payment.d.ts.map +1 -1
- package/dist/payment/payment.js +387 -47
- package/dist/payment/payment.js.map +1 -1
- package/dist/registry/db.d.ts.map +1 -1
- package/dist/registry/db.js +110 -3
- package/dist/registry/db.js.map +1 -1
- package/dist/registry/registry.d.ts +1 -1
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +12 -4
- package/dist/registry/registry.js.map +1 -1
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/validation.d.ts +27 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +164 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/verification/receipt.d.ts +42 -0
- package/dist/verification/receipt.d.ts.map +1 -0
- package/dist/verification/receipt.js +10 -0
- package/dist/verification/receipt.js.map +1 -0
- package/dist/verification/verification.d.ts +41 -0
- package/dist/verification/verification.d.ts.map +1 -0
- package/dist/verification/verification.js +217 -0
- package/dist/verification/verification.js.map +1 -0
- package/dist/wallet/providerManager.d.ts +32 -0
- package/dist/wallet/providerManager.d.ts.map +1 -0
- package/dist/wallet/providerManager.js +118 -0
- package/dist/wallet/providerManager.js.map +1 -0
- package/dist/wallet/providers/handcash.d.ts +22 -0
- package/dist/wallet/providers/handcash.d.ts.map +1 -0
- package/dist/wallet/providers/handcash.js +214 -0
- package/dist/wallet/providers/handcash.js.map +1 -0
- package/dist/wallet/providers/internal.d.ts +15 -0
- package/dist/wallet/providers/internal.d.ts.map +1 -0
- package/dist/wallet/providers/internal.js +208 -0
- package/dist/wallet/providers/internal.js.map +1 -0
- package/dist/wallet/providers/types.d.ts +50 -0
- package/dist/wallet/providers/types.d.ts.map +1 -0
- package/dist/wallet/providers/types.js +6 -0
- package/dist/wallet/providers/types.js.map +1 -0
- package/dist/wallet/providers/yours.d.ts +18 -0
- package/dist/wallet/providers/yours.d.ts.map +1 -0
- package/dist/wallet/providers/yours.js +122 -0
- package/dist/wallet/providers/yours.js.map +1 -0
- package/dist/wallet/wallet.d.ts +52 -5
- package/dist/wallet/wallet.d.ts.map +1 -1
- package/dist/wallet/wallet.js +223 -34
- package/dist/wallet/wallet.js.map +1 -1
- package/dist/webhooks/delivery.d.ts +37 -0
- package/dist/webhooks/delivery.d.ts.map +1 -0
- package/dist/webhooks/delivery.js +182 -0
- package/dist/webhooks/delivery.js.map +1 -0
- package/dist/webhooks/webhook.d.ts +85 -0
- package/dist/webhooks/webhook.d.ts.map +1 -0
- package/dist/webhooks/webhook.js +271 -0
- package/dist/webhooks/webhook.js.map +1 -0
- package/package.json +74 -54
- package/sdk-python/LICENSE +21 -0
- package/sdk-python/MANIFEST.in +9 -0
- package/sdk-python/README.md +372 -0
- package/sdk-python/agentspay/__init__.py +97 -0
- package/sdk-python/agentspay/client.py +256 -0
- package/sdk-python/agentspay/disputes.py +174 -0
- package/sdk-python/agentspay/exceptions.py +53 -0
- package/sdk-python/agentspay/payments.py +169 -0
- package/sdk-python/agentspay/services.py +198 -0
- package/sdk-python/agentspay/types.py +154 -0
- package/sdk-python/agentspay/wallet.py +113 -0
- package/sdk-python/agentspay/webhooks.py +195 -0
- package/sdk-python/examples/consumer.py +147 -0
- package/sdk-python/examples/provider.py +116 -0
- package/sdk-python/pyproject.toml +61 -0
- package/sdk-python/setup.py +53 -0
- package/sdk-python/tests/test_client.py +221 -0
- package/test-addr.js +29 -0
- package/test-mnee-simple.js +51 -0
- package/test-mnee.js +47 -0
package/dist/api/server.js
CHANGED
|
@@ -39,31 +39,228 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.app = void 0;
|
|
40
40
|
exports.startServer = startServer;
|
|
41
41
|
const express_1 = __importDefault(require("express"));
|
|
42
|
+
const cors_1 = __importDefault(require("cors"));
|
|
43
|
+
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
44
|
+
const net_1 = __importDefault(require("net"));
|
|
42
45
|
const wallet_1 = require("../wallet/wallet");
|
|
43
46
|
const registry_1 = require("../registry/registry");
|
|
44
47
|
const payment_1 = require("../payment/payment");
|
|
48
|
+
const db_1 = require("../registry/db");
|
|
49
|
+
const webhook_1 = require("../webhooks/webhook");
|
|
50
|
+
const delivery_1 = require("../webhooks/delivery");
|
|
51
|
+
const dispute_1 = require("../disputes/dispute");
|
|
52
|
+
const swagger_1 = require("../docs/swagger");
|
|
53
|
+
const mnee_1 = require("../bsv/mnee");
|
|
54
|
+
const currency_1 = require("../currency/currency");
|
|
55
|
+
const verification_1 = require("../verification/verification");
|
|
45
56
|
const app = (0, express_1.default)();
|
|
46
57
|
exports.app = app;
|
|
58
|
+
app.disable('x-powered-by');
|
|
59
|
+
app.use((0, cors_1.default)());
|
|
47
60
|
app.use(express_1.default.json());
|
|
61
|
+
// Setup Swagger UI documentation at /docs
|
|
62
|
+
(0, swagger_1.setupSwagger)(app);
|
|
63
|
+
// Basic API rate limiting (in-memory)
|
|
64
|
+
app.use('/api', (0, express_rate_limit_1.default)({
|
|
65
|
+
windowMs: 60_000,
|
|
66
|
+
limit: 100,
|
|
67
|
+
standardHeaders: 'draft-7',
|
|
68
|
+
legacyHeaders: false,
|
|
69
|
+
}));
|
|
48
70
|
const wallets = new wallet_1.WalletManager();
|
|
49
71
|
const registry = new registry_1.Registry();
|
|
50
72
|
const payments = new payment_1.PaymentEngine();
|
|
73
|
+
const webhooks = new webhook_1.WebhookManager();
|
|
74
|
+
const disputes = new dispute_1.DisputeManager();
|
|
75
|
+
const verification = new verification_1.VerificationManager();
|
|
76
|
+
function getApiKey(req) {
|
|
77
|
+
const key = req.header('x-api-key') || req.header('authorization');
|
|
78
|
+
if (!key)
|
|
79
|
+
return null;
|
|
80
|
+
const m = key.match(/^Bearer\s+(.+)$/i);
|
|
81
|
+
return m ? m[1].trim() : key.trim();
|
|
82
|
+
}
|
|
83
|
+
function requireApiKey(req, res, next) {
|
|
84
|
+
const apiKey = getApiKey(req);
|
|
85
|
+
if (!apiKey)
|
|
86
|
+
return res.status(401).json({ error: 'API key required' });
|
|
87
|
+
const wallet = wallets.getByApiKey(apiKey);
|
|
88
|
+
if (!wallet)
|
|
89
|
+
return res.status(401).json({ error: 'Invalid API key' });
|
|
90
|
+
req.authWallet = wallet;
|
|
91
|
+
req.apiKey = apiKey;
|
|
92
|
+
next();
|
|
93
|
+
}
|
|
94
|
+
function requireWalletMatch(req, res, next) {
|
|
95
|
+
const auth = req.authWallet;
|
|
96
|
+
if (!auth)
|
|
97
|
+
return res.status(500).json({ error: 'Auth context missing' });
|
|
98
|
+
if (req.params.id !== auth.id)
|
|
99
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
100
|
+
next();
|
|
101
|
+
}
|
|
102
|
+
function isPrivateIp(ip) {
|
|
103
|
+
// IPv4 only (sufficient for audit scenarios)
|
|
104
|
+
const parts = ip.split('.').map(p => Number(p));
|
|
105
|
+
if (parts.length !== 4 || parts.some(n => !Number.isFinite(n)))
|
|
106
|
+
return false;
|
|
107
|
+
const [a, b] = parts;
|
|
108
|
+
if (a === 10)
|
|
109
|
+
return true;
|
|
110
|
+
if (a === 127)
|
|
111
|
+
return true;
|
|
112
|
+
if (a === 0)
|
|
113
|
+
return true;
|
|
114
|
+
if (a === 169 && b === 254)
|
|
115
|
+
return true;
|
|
116
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
117
|
+
return true;
|
|
118
|
+
if (a === 192 && b === 168)
|
|
119
|
+
return true;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function validateServiceEndpoint(endpoint) {
|
|
123
|
+
let u;
|
|
124
|
+
try {
|
|
125
|
+
u = new URL(endpoint);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new Error('Invalid endpoint URL');
|
|
129
|
+
}
|
|
130
|
+
if (!['http:', 'https:'].includes(u.protocol))
|
|
131
|
+
throw new Error('Endpoint must be http(s)');
|
|
132
|
+
const host = u.hostname.toLowerCase();
|
|
133
|
+
if (host === 'localhost' || host === '0.0.0.0')
|
|
134
|
+
throw new Error('Endpoint host not allowed');
|
|
135
|
+
if (host === '169.254.169.254')
|
|
136
|
+
throw new Error('Endpoint host not allowed');
|
|
137
|
+
if (net_1.default.isIP(host)) {
|
|
138
|
+
if (isPrivateIp(host))
|
|
139
|
+
throw new Error('Endpoint IP not allowed');
|
|
140
|
+
}
|
|
141
|
+
const port = u.port ? Number(u.port) : u.protocol === 'https:' ? 443 : 80;
|
|
142
|
+
if (![80, 443].includes(port))
|
|
143
|
+
throw new Error('Endpoint port not allowed');
|
|
144
|
+
}
|
|
51
145
|
// ============ WALLETS ============
|
|
52
|
-
|
|
146
|
+
// Create wallet + API key for demo/internal usage
|
|
147
|
+
app.post('/api/wallets/connect/internal', (_req, res) => {
|
|
53
148
|
const wallet = wallets.create();
|
|
54
|
-
res.json({
|
|
149
|
+
res.json({
|
|
150
|
+
ok: true,
|
|
151
|
+
wallet: { id: wallet.id, publicKey: wallet.publicKey, address: wallet.address, createdAt: wallet.createdAt },
|
|
152
|
+
apiKey: wallet.apiKey,
|
|
153
|
+
privateKey: wallet.privateKey,
|
|
154
|
+
});
|
|
55
155
|
});
|
|
56
|
-
app.
|
|
57
|
-
const wallet = wallets.
|
|
156
|
+
app.post('/api/wallets', (_req, res) => {
|
|
157
|
+
const wallet = wallets.create();
|
|
158
|
+
res.json({
|
|
159
|
+
ok: true,
|
|
160
|
+
wallet: { id: wallet.id, publicKey: wallet.publicKey, address: wallet.address, createdAt: wallet.createdAt },
|
|
161
|
+
apiKey: wallet.apiKey,
|
|
162
|
+
privateKey: wallet.privateKey,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
app.post('/api/wallets/import', (req, res) => {
|
|
166
|
+
const { wif } = req.body;
|
|
167
|
+
if (!wif)
|
|
168
|
+
return res.status(400).json({ error: 'WIF private key required' });
|
|
169
|
+
try {
|
|
170
|
+
const wallet = wallets.importFromWif(wif);
|
|
171
|
+
res.json({ ok: true, wallet });
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
res.status(400).json({ error: e.message });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
app.get('/api/wallets/:id', requireApiKey, requireWalletMatch, async (req, res) => {
|
|
178
|
+
const wallet = wallets.getById(String(req.params.id));
|
|
58
179
|
if (!wallet)
|
|
59
180
|
return res.status(404).json({ error: 'Wallet not found' });
|
|
60
|
-
const balance = await wallets.getBalance(req.params.id);
|
|
61
|
-
|
|
181
|
+
const balance = await wallets.getBalance(String(req.params.id));
|
|
182
|
+
const balanceMnee = await mnee_1.mneeTokens.getBalance(wallet.address);
|
|
183
|
+
res.json({
|
|
184
|
+
ok: true,
|
|
185
|
+
wallet: {
|
|
186
|
+
...wallet,
|
|
187
|
+
balance,
|
|
188
|
+
balanceBsv: balance,
|
|
189
|
+
balanceMnee,
|
|
190
|
+
balances: {
|
|
191
|
+
BSV: { amount: balance, formatted: currency_1.CurrencyManager.format(balance, 'BSV') },
|
|
192
|
+
MNEE: { amount: balanceMnee, formatted: currency_1.CurrencyManager.format(balanceMnee, 'MNEE') }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
// ============ CURRENCY ============
|
|
198
|
+
app.get('/api/rates', async (_req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
const bsvToMnee = await currency_1.CurrencyManager.getConversionRate('BSV', 'MNEE');
|
|
201
|
+
const mneeToBsv = await currency_1.CurrencyManager.getConversionRate('MNEE', 'BSV');
|
|
202
|
+
res.json({
|
|
203
|
+
ok: true,
|
|
204
|
+
rates: {
|
|
205
|
+
BSV_to_MNEE: bsvToMnee,
|
|
206
|
+
MNEE_to_BSV: mneeToBsv
|
|
207
|
+
},
|
|
208
|
+
currencies: {
|
|
209
|
+
BSV: currency_1.CurrencyManager.getConfig('BSV'),
|
|
210
|
+
MNEE: currency_1.CurrencyManager.getConfig('MNEE')
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
res.status(500).json({ error: e.message });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
app.post('/api/wallets/:id/fund-mnee', requireApiKey, requireWalletMatch, async (req, res) => {
|
|
219
|
+
try {
|
|
220
|
+
const { amount } = req.body;
|
|
221
|
+
const wallet = wallets.getById(String(req.params.id));
|
|
222
|
+
if (!wallet)
|
|
223
|
+
return res.status(404).json({ error: 'Wallet not found' });
|
|
224
|
+
const amountCents = Number(amount);
|
|
225
|
+
if (!Number.isInteger(amountCents) || amountCents <= 0) {
|
|
226
|
+
throw new Error('Invalid amount (must be positive integer in cents)');
|
|
227
|
+
}
|
|
228
|
+
await mnee_1.mneeTokens.fundDemo(wallet.address, amountCents);
|
|
229
|
+
const newBalance = await mnee_1.mneeTokens.getBalance(wallet.address);
|
|
230
|
+
res.json({
|
|
231
|
+
ok: true,
|
|
232
|
+
message: `Funded ${amountCents} MNEE cents ($${(amountCents / 100).toFixed(2)})`,
|
|
233
|
+
balance: newBalance,
|
|
234
|
+
balanceFormatted: currency_1.CurrencyManager.format(newBalance, 'MNEE')
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
res.status(400).json({ error: e.message });
|
|
239
|
+
}
|
|
62
240
|
});
|
|
63
241
|
// ============ SERVICES (Registry) ============
|
|
64
|
-
app.post('/api/services', (req, res) => {
|
|
242
|
+
app.post('/api/services', requireApiKey, (req, res) => {
|
|
65
243
|
try {
|
|
66
|
-
const
|
|
244
|
+
const auth = req.authWallet;
|
|
245
|
+
if (req.body?.agentId !== auth.id)
|
|
246
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
247
|
+
if (typeof req.body?.name !== 'string' || req.body.name.length < 1 || req.body.name.length > 120) {
|
|
248
|
+
throw new Error('Invalid name');
|
|
249
|
+
}
|
|
250
|
+
if (typeof req.body?.description !== 'string' || req.body.description.length < 1 || req.body.description.length > 2000) {
|
|
251
|
+
throw new Error('Invalid description');
|
|
252
|
+
}
|
|
253
|
+
if (/<script\b/i.test(req.body.description))
|
|
254
|
+
throw new Error('Invalid description');
|
|
255
|
+
const price = Number(req.body?.price);
|
|
256
|
+
if (!Number.isInteger(price) || price <= 0 || price > 100000000)
|
|
257
|
+
throw new Error('Invalid price');
|
|
258
|
+
const currency = req.body?.currency || 'BSV';
|
|
259
|
+
if (currency !== 'BSV' && currency !== 'MNEE') {
|
|
260
|
+
throw new Error('Invalid currency. Must be BSV or MNEE');
|
|
261
|
+
}
|
|
262
|
+
validateServiceEndpoint(String(req.body?.endpoint));
|
|
263
|
+
const service = registry.register({ ...req.body, currency });
|
|
67
264
|
res.json({ ok: true, service });
|
|
68
265
|
}
|
|
69
266
|
catch (e) {
|
|
@@ -72,8 +269,8 @@ app.post('/api/services', (req, res) => {
|
|
|
72
269
|
});
|
|
73
270
|
app.get('/api/services', (req, res) => {
|
|
74
271
|
const services = registry.search({
|
|
75
|
-
category: req.query.category,
|
|
76
|
-
keyword: req.query.q,
|
|
272
|
+
category: typeof req.query.category === 'string' ? req.query.category : undefined,
|
|
273
|
+
keyword: typeof req.query.q === 'string' ? req.query.q : undefined,
|
|
77
274
|
maxPrice: req.query.maxPrice ? Number(req.query.maxPrice) : undefined,
|
|
78
275
|
limit: req.query.limit ? Number(req.query.limit) : undefined,
|
|
79
276
|
offset: req.query.offset ? Number(req.query.offset) : undefined,
|
|
@@ -81,15 +278,32 @@ app.get('/api/services', (req, res) => {
|
|
|
81
278
|
res.json({ ok: true, services });
|
|
82
279
|
});
|
|
83
280
|
app.get('/api/services/:id', (req, res) => {
|
|
84
|
-
const service = registry.getById(req.params.id);
|
|
281
|
+
const service = registry.getById(String(req.params.id));
|
|
85
282
|
if (!service)
|
|
86
283
|
return res.status(404).json({ error: 'Service not found' });
|
|
87
284
|
res.json({ ok: true, service });
|
|
88
285
|
});
|
|
89
|
-
app.patch('/api/services/:id', (req, res) => {
|
|
90
|
-
const
|
|
91
|
-
|
|
286
|
+
app.patch('/api/services/:id', requireApiKey, (req, res) => {
|
|
287
|
+
const auth = req.authWallet;
|
|
288
|
+
const existing = registry.getById(String(req.params.id));
|
|
289
|
+
if (!existing)
|
|
92
290
|
return res.status(404).json({ error: 'Service not found' });
|
|
291
|
+
if (existing.agentId !== auth.id)
|
|
292
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
293
|
+
if (req.body?.price !== undefined) {
|
|
294
|
+
const price = Number(req.body.price);
|
|
295
|
+
if (!Number.isInteger(price) || price <= 0 || price > 100000000)
|
|
296
|
+
return res.status(400).json({ error: 'Invalid price' });
|
|
297
|
+
}
|
|
298
|
+
if (req.body?.endpoint) {
|
|
299
|
+
try {
|
|
300
|
+
validateServiceEndpoint(String(req.body.endpoint));
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
return res.status(400).json({ error: e.message });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const service = registry.update(String(req.params.id), req.body);
|
|
93
307
|
res.json({ ok: true, service });
|
|
94
308
|
});
|
|
95
309
|
// ============ EXECUTE (Pay + Run) ============
|
|
@@ -100,116 +314,358 @@ app.post('/api/execute/:serviceId', async (req, res) => {
|
|
|
100
314
|
return res.status(404).json({ error: 'Service not found' });
|
|
101
315
|
if (!service.active)
|
|
102
316
|
return res.status(400).json({ error: 'Service is inactive' });
|
|
317
|
+
try {
|
|
318
|
+
validateServiceEndpoint(service.endpoint);
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
return res.status(400).json({ error: `Unsafe service endpoint: ${e.message}` });
|
|
322
|
+
}
|
|
103
323
|
const buyer = wallets.getById(buyerWalletId);
|
|
104
324
|
if (!buyer)
|
|
105
325
|
return res.status(404).json({ error: 'Buyer wallet not found' });
|
|
106
|
-
|
|
107
|
-
|
|
326
|
+
const currency = service.currency || 'BSV';
|
|
327
|
+
// Check balance based on currency
|
|
328
|
+
let balance = 0;
|
|
329
|
+
if (currency === 'BSV') {
|
|
330
|
+
balance = await wallets.getBalance(buyerWalletId);
|
|
331
|
+
}
|
|
332
|
+
else if (currency === 'MNEE') {
|
|
333
|
+
balance = await mnee_1.mneeTokens.getBalance(buyer.address);
|
|
334
|
+
}
|
|
108
335
|
if (balance < service.price) {
|
|
109
336
|
return res.status(402).json({
|
|
110
|
-
error:
|
|
337
|
+
error: `Insufficient ${currency} balance`,
|
|
111
338
|
required: service.price,
|
|
339
|
+
requiredFormatted: currency_1.CurrencyManager.format(service.price, currency),
|
|
112
340
|
available: balance,
|
|
113
|
-
|
|
341
|
+
availableFormatted: currency_1.CurrencyManager.format(balance, currency),
|
|
342
|
+
currency,
|
|
343
|
+
address: buyer.address,
|
|
114
344
|
});
|
|
115
345
|
}
|
|
116
|
-
// Create escrow payment
|
|
117
|
-
const payment = payments.create(service.id, buyerWalletId, service.agentId, service.price);
|
|
118
|
-
// Execute the service
|
|
119
|
-
const startTime = Date.now();
|
|
120
346
|
try {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
347
|
+
const payment = await payments.create(service.id, buyerWalletId, service.agentId, service.price, currency);
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
const timeoutMs = (service.timeout || 30) * 1000;
|
|
350
|
+
try {
|
|
351
|
+
// Execute with timeout
|
|
352
|
+
const controller = new AbortController();
|
|
353
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
354
|
+
const response = await fetch(service.endpoint, {
|
|
355
|
+
method: service.method,
|
|
356
|
+
headers: { 'Content-Type': 'application/json' },
|
|
357
|
+
body: JSON.stringify(input),
|
|
358
|
+
signal: controller.signal,
|
|
359
|
+
}).finally(() => clearTimeout(timeoutId));
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
await payments.refund(payment.id);
|
|
362
|
+
return res.status(502).json({
|
|
363
|
+
error: 'Service execution failed',
|
|
364
|
+
paymentId: payment.id,
|
|
365
|
+
status: 'refunded',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const output = await response.json();
|
|
369
|
+
const executionTimeMs = Date.now() - startTime;
|
|
370
|
+
// Mark payment as completed (starts dispute window)
|
|
371
|
+
const db = (0, db_1.getDb)();
|
|
372
|
+
db.prepare('UPDATE payments SET completedAt = ? WHERE id = ?').run(new Date().toISOString(), payment.id);
|
|
373
|
+
// Create execution receipt (cryptographic proof)
|
|
374
|
+
const receipt = await verification.createReceipt(payment, input || {}, output || {}, executionTimeMs);
|
|
375
|
+
// Release payment immediately (buyer has dispute window to file dispute)
|
|
376
|
+
await payments.release(payment.id);
|
|
377
|
+
// Trigger service.executed webhook (include receipt)
|
|
378
|
+
delivery_1.webhookDelivery.trigger('service.executed', {
|
|
379
|
+
serviceId: service.id,
|
|
380
|
+
paymentId: payment.id,
|
|
381
|
+
executionTimeMs,
|
|
382
|
+
output,
|
|
383
|
+
receipt,
|
|
384
|
+
}).catch(console.error);
|
|
385
|
+
// Check for expired dispute windows
|
|
386
|
+
disputes.checkExpiredWindows();
|
|
387
|
+
res.json({
|
|
388
|
+
ok: true,
|
|
131
389
|
paymentId: payment.id,
|
|
132
|
-
|
|
390
|
+
output,
|
|
391
|
+
executionTimeMs,
|
|
392
|
+
cost: {
|
|
393
|
+
amount: service.price,
|
|
394
|
+
amountFormatted: currency_1.CurrencyManager.format(service.price, currency),
|
|
395
|
+
platformFee: payment.platformFee,
|
|
396
|
+
platformFeeFormatted: currency_1.CurrencyManager.format(payment.platformFee, currency),
|
|
397
|
+
currency,
|
|
398
|
+
},
|
|
399
|
+
txId: payment.txId,
|
|
400
|
+
disputeWindowMinutes: service.disputeWindow || 30,
|
|
401
|
+
receipt,
|
|
133
402
|
});
|
|
134
403
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
404
|
+
catch (e) {
|
|
405
|
+
// Handle timeout or service failure
|
|
406
|
+
await payments.refund(payment.id);
|
|
407
|
+
if (e.name === 'AbortError') {
|
|
408
|
+
res.status(504).json({
|
|
409
|
+
error: `Service timeout after ${service.timeout || 30}s`,
|
|
410
|
+
paymentId: payment.id,
|
|
411
|
+
status: 'refunded',
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
res.status(502).json({
|
|
416
|
+
error: `Service unreachable: ${e.message}`,
|
|
417
|
+
paymentId: payment.id,
|
|
418
|
+
status: 'refunded',
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
150
422
|
}
|
|
151
423
|
catch (e) {
|
|
152
|
-
|
|
153
|
-
payments.refund(payment.id);
|
|
154
|
-
res.status(502).json({
|
|
155
|
-
error: `Service unreachable: ${e.message}`,
|
|
156
|
-
paymentId: payment.id,
|
|
157
|
-
status: 'refunded',
|
|
158
|
-
});
|
|
424
|
+
res.status(500).json({ error: `Payment creation failed: ${e.message}` });
|
|
159
425
|
}
|
|
160
426
|
});
|
|
161
427
|
// ============ PAYMENTS ============
|
|
162
428
|
app.get('/api/payments/:id', (req, res) => {
|
|
163
|
-
const payment = payments.getById(req.params.id);
|
|
429
|
+
const payment = payments.getById(String(req.params.id));
|
|
164
430
|
if (!payment)
|
|
165
431
|
return res.status(404).json({ error: 'Payment not found' });
|
|
166
432
|
res.json({ ok: true, payment });
|
|
167
433
|
});
|
|
168
434
|
app.post('/api/payments/:id/dispute', (req, res) => {
|
|
169
|
-
const payment = payments.dispute(req.params.id);
|
|
435
|
+
const payment = payments.dispute(String(req.params.id));
|
|
170
436
|
if (!payment)
|
|
171
437
|
return res.status(400).json({ error: 'Cannot dispute this payment' });
|
|
172
438
|
res.json({ ok: true, payment });
|
|
173
439
|
});
|
|
440
|
+
// ============ DISPUTES ============
|
|
441
|
+
app.post('/api/disputes', requireApiKey, (req, res) => {
|
|
442
|
+
try {
|
|
443
|
+
const auth = req.authWallet;
|
|
444
|
+
const { paymentId, reason, evidence } = req.body;
|
|
445
|
+
if (!paymentId || typeof paymentId !== 'string') {
|
|
446
|
+
return res.status(400).json({ error: 'paymentId required' });
|
|
447
|
+
}
|
|
448
|
+
if (!reason || typeof reason !== 'string' || reason.length < 10 || reason.length > 2000) {
|
|
449
|
+
return res.status(400).json({ error: 'Reason required (10-2000 chars)' });
|
|
450
|
+
}
|
|
451
|
+
if (evidence !== undefined && (typeof evidence !== 'string' || evidence.length > 10000)) {
|
|
452
|
+
return res.status(400).json({ error: 'Evidence too long (max 10000 chars)' });
|
|
453
|
+
}
|
|
454
|
+
const dispute = disputes.create(paymentId, auth.id, reason, evidence);
|
|
455
|
+
res.json({ ok: true, dispute });
|
|
456
|
+
}
|
|
457
|
+
catch (e) {
|
|
458
|
+
res.status(400).json({ error: e.message });
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
app.get('/api/disputes/:id', requireApiKey, (req, res) => {
|
|
462
|
+
const dispute = disputes.getById(String(req.params.id));
|
|
463
|
+
if (!dispute)
|
|
464
|
+
return res.status(404).json({ error: 'Dispute not found' });
|
|
465
|
+
const auth = req.authWallet;
|
|
466
|
+
if (dispute.buyerWalletId !== auth.id && dispute.providerWalletId !== auth.id) {
|
|
467
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
468
|
+
}
|
|
469
|
+
res.json({ ok: true, dispute });
|
|
470
|
+
});
|
|
471
|
+
app.get('/api/disputes', requireApiKey, (req, res) => {
|
|
472
|
+
const auth = req.authWallet;
|
|
473
|
+
const status = typeof req.query.status === 'string' ? req.query.status : undefined;
|
|
474
|
+
const disputesList = disputes.listByWallet(auth.id, status);
|
|
475
|
+
res.json({ ok: true, disputes: disputesList });
|
|
476
|
+
});
|
|
477
|
+
app.post('/api/disputes/:id/resolve', requireApiKey, (req, res) => {
|
|
478
|
+
try {
|
|
479
|
+
// TODO: Add admin check here (for now, any authenticated user can resolve)
|
|
480
|
+
// In production, this should be restricted to platform admins
|
|
481
|
+
const { resolution, splitPercent } = req.body;
|
|
482
|
+
if (!resolution || !['refund', 'release', 'split'].includes(resolution)) {
|
|
483
|
+
return res.status(400).json({ error: 'Invalid resolution. Must be: refund, release, or split' });
|
|
484
|
+
}
|
|
485
|
+
const dispute = disputes.resolve(String(req.params.id), { resolution, splitPercent });
|
|
486
|
+
if (!dispute)
|
|
487
|
+
return res.status(404).json({ error: 'Dispute not found' });
|
|
488
|
+
res.json({ ok: true, dispute });
|
|
489
|
+
}
|
|
490
|
+
catch (e) {
|
|
491
|
+
res.status(400).json({ error: e.message });
|
|
492
|
+
}
|
|
493
|
+
});
|
|
174
494
|
// ============ REPUTATION ============
|
|
175
495
|
app.get('/api/agents/:id/reputation', (req, res) => {
|
|
176
|
-
const reputation = registry.getReputation(req.params.id);
|
|
496
|
+
const reputation = registry.getReputation(String(req.params.id));
|
|
177
497
|
res.json({ ok: true, reputation });
|
|
178
498
|
});
|
|
499
|
+
// ============ WALLET UTXOS ============
|
|
500
|
+
app.get('/api/wallets/:id/utxos', async (req, res) => {
|
|
501
|
+
const apiKey = getApiKey(req);
|
|
502
|
+
if (!apiKey)
|
|
503
|
+
return res.status(401).json({ error: 'API key required' });
|
|
504
|
+
if (!wallets.verifyApiKey(String(req.params.id), apiKey))
|
|
505
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
506
|
+
const wallet = wallets.getById(String(req.params.id));
|
|
507
|
+
if (!wallet)
|
|
508
|
+
return res.status(404).json({ error: 'Wallet not found' });
|
|
509
|
+
try {
|
|
510
|
+
const utxos = await wallets.getUtxos(String(req.params.id));
|
|
511
|
+
res.json({ ok: true, utxos });
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
res.status(500).json({ error: e.message });
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
// ============ WALLET TX HISTORY ============
|
|
518
|
+
app.get('/api/wallets/:id/transactions', async (req, res) => {
|
|
519
|
+
const apiKey = getApiKey(req);
|
|
520
|
+
if (!apiKey)
|
|
521
|
+
return res.status(401).json({ error: 'API key required' });
|
|
522
|
+
if (!wallets.verifyApiKey(String(req.params.id), apiKey))
|
|
523
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
524
|
+
const wallet = wallets.getById(String(req.params.id));
|
|
525
|
+
if (!wallet)
|
|
526
|
+
return res.status(404).json({ error: 'Wallet not found' });
|
|
527
|
+
try {
|
|
528
|
+
const transactions = await wallets.getTxHistory(String(req.params.id));
|
|
529
|
+
res.json({ ok: true, transactions });
|
|
530
|
+
}
|
|
531
|
+
catch (e) {
|
|
532
|
+
res.status(500).json({ error: e.message });
|
|
533
|
+
}
|
|
534
|
+
});
|
|
179
535
|
// ============ FUND (testnet/demo only) ============
|
|
180
|
-
app.post('/api/wallets/:id/fund', async (req, res) => {
|
|
181
|
-
const { amount } = req.body;
|
|
182
|
-
if (!amount || amount <= 0)
|
|
536
|
+
app.post('/api/wallets/:id/fund', requireApiKey, requireWalletMatch, async (req, res) => {
|
|
537
|
+
const { amount } = req.body;
|
|
538
|
+
if (!Number.isInteger(amount) || amount <= 0 || amount > 100000000)
|
|
183
539
|
return res.status(400).json({ error: 'Invalid amount' });
|
|
184
|
-
const wallet = wallets.getById(req.params.id);
|
|
540
|
+
const wallet = wallets.getById(String(req.params.id));
|
|
185
541
|
if (!wallet)
|
|
186
542
|
return res.status(404).json({ error: 'Wallet not found' });
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const db = getDatabase();
|
|
191
|
-
// Ensure deposits table exists
|
|
192
|
-
db.exec(`CREATE TABLE IF NOT EXISTS deposits (
|
|
193
|
-
id TEXT PRIMARY KEY, walletId TEXT NOT NULL, amount INTEGER NOT NULL, createdAt TEXT NOT NULL
|
|
543
|
+
const db = (0, db_1.getDb)();
|
|
544
|
+
db.exec(`CREATE TABLE IF NOT EXISTS deposits (
|
|
545
|
+
id TEXT PRIMARY KEY, walletId TEXT NOT NULL, amount INTEGER NOT NULL, createdAt TEXT NOT NULL
|
|
194
546
|
)`);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const balance = await wallets.getBalance(req.params.id);
|
|
198
|
-
res.json({ ok: true, funded: amount, balance });
|
|
547
|
+
const { v4: uuidv4 } = await Promise.resolve().then(() => __importStar(require('uuid')));
|
|
548
|
+
db.prepare(`INSERT INTO deposits (id, walletId, amount, createdAt) VALUES (?, ?, ?, datetime('now'))`).run(uuidv4(), String(req.params.id), amount);
|
|
549
|
+
const balance = await wallets.getBalance(String(req.params.id));
|
|
550
|
+
res.json({ ok: true, funded: amount, balance, mode: 'internal-ledger' });
|
|
551
|
+
});
|
|
552
|
+
// ============ WEBHOOKS ============
|
|
553
|
+
app.post('/api/webhooks', requireApiKey, (req, res) => {
|
|
554
|
+
try {
|
|
555
|
+
const auth = req.authWallet;
|
|
556
|
+
const { url, events } = req.body;
|
|
557
|
+
if (!url || typeof url !== 'string') {
|
|
558
|
+
return res.status(400).json({ error: 'Valid URL required' });
|
|
559
|
+
}
|
|
560
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
561
|
+
return res.status(400).json({ error: 'At least one event required' });
|
|
562
|
+
}
|
|
563
|
+
const webhook = webhooks.register({ url, events, ownerId: auth.id });
|
|
564
|
+
res.json({ ok: true, webhook });
|
|
565
|
+
}
|
|
566
|
+
catch (e) {
|
|
567
|
+
res.status(400).json({ error: e.message });
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
app.get('/api/webhooks', requireApiKey, (req, res) => {
|
|
571
|
+
const auth = req.authWallet;
|
|
572
|
+
const webhookList = webhooks.list(auth.id);
|
|
573
|
+
res.json({ ok: true, webhooks: webhookList });
|
|
574
|
+
});
|
|
575
|
+
app.put('/api/webhooks/:id', requireApiKey, (req, res) => {
|
|
576
|
+
try {
|
|
577
|
+
const auth = req.authWallet;
|
|
578
|
+
const { url, events, active } = req.body;
|
|
579
|
+
const updates = {};
|
|
580
|
+
if (url !== undefined) {
|
|
581
|
+
if (typeof url !== 'string') {
|
|
582
|
+
return res.status(400).json({ error: 'URL must be a string' });
|
|
583
|
+
}
|
|
584
|
+
updates.url = url;
|
|
585
|
+
}
|
|
586
|
+
if (events !== undefined) {
|
|
587
|
+
if (!Array.isArray(events)) {
|
|
588
|
+
return res.status(400).json({ error: 'Events must be an array' });
|
|
589
|
+
}
|
|
590
|
+
updates.events = events;
|
|
591
|
+
}
|
|
592
|
+
if (active !== undefined)
|
|
593
|
+
updates.active = active;
|
|
594
|
+
const webhook = webhooks.update(String(req.params.id), updates, auth.id);
|
|
595
|
+
if (!webhook)
|
|
596
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
597
|
+
res.json({ ok: true, webhook });
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
res.status(400).json({ error: e.message });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
app.delete('/api/webhooks/:id', requireApiKey, (req, res) => {
|
|
604
|
+
try {
|
|
605
|
+
const auth = req.authWallet;
|
|
606
|
+
const deleted = webhooks.delete(req.params.id, auth.id);
|
|
607
|
+
if (!deleted)
|
|
608
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
609
|
+
res.json({ ok: true });
|
|
610
|
+
}
|
|
611
|
+
catch (e) {
|
|
612
|
+
res.status(400).json({ error: e.message });
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
// ============ RECEIPTS (Execution Verification) ============
|
|
616
|
+
app.get('/api/receipts/:paymentId', (req, res) => {
|
|
617
|
+
try {
|
|
618
|
+
const receipt = verification.getReceipt(String(req.params.paymentId));
|
|
619
|
+
if (!receipt) {
|
|
620
|
+
return res.status(404).json({ error: 'Receipt not found' });
|
|
621
|
+
}
|
|
622
|
+
res.json({ ok: true, receipt });
|
|
623
|
+
}
|
|
624
|
+
catch (e) {
|
|
625
|
+
res.status(500).json({ error: e.message });
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
app.get('/api/receipts/:paymentId/verify', async (req, res) => {
|
|
629
|
+
try {
|
|
630
|
+
const receipt = verification.getReceipt(String(req.params.paymentId));
|
|
631
|
+
if (!receipt) {
|
|
632
|
+
return res.status(404).json({ error: 'Receipt not found' });
|
|
633
|
+
}
|
|
634
|
+
const result = await verification.verifyReceipt(receipt);
|
|
635
|
+
res.json({
|
|
636
|
+
ok: true,
|
|
637
|
+
verification: result,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
catch (e) {
|
|
641
|
+
res.status(500).json({ error: e.message });
|
|
642
|
+
}
|
|
199
643
|
});
|
|
200
644
|
// ============ HEALTH ============
|
|
201
645
|
app.get('/api/health', (_req, res) => {
|
|
202
646
|
res.json({ ok: true, service: 'agentpay', version: '0.1.0' });
|
|
203
647
|
});
|
|
204
|
-
//
|
|
648
|
+
// JSON parse error handler (avoid stack traces)
|
|
649
|
+
app.use((err, _req, res, next) => {
|
|
650
|
+
if (err && err.type === 'entity.parse.failed') {
|
|
651
|
+
return res.status(400).json({ error: 'Invalid JSON' });
|
|
652
|
+
}
|
|
653
|
+
if (err instanceof SyntaxError && 'body' in err) {
|
|
654
|
+
return res.status(400).json({ error: 'Invalid JSON' });
|
|
655
|
+
}
|
|
656
|
+
return next(err);
|
|
657
|
+
});
|
|
205
658
|
const PORT = Number(process.env.PORT) || 3100;
|
|
206
659
|
function startServer() {
|
|
207
660
|
app.listen(PORT, () => {
|
|
208
661
|
console.log(`🚀 AgentPay API running on http://localhost:${PORT}`);
|
|
662
|
+
console.log(`📚 API Docs: http://localhost:${PORT}/docs`);
|
|
209
663
|
console.log(`📋 Registry: GET /api/services`);
|
|
210
664
|
console.log(`💰 Execute: POST /api/execute/:serviceId`);
|
|
211
665
|
console.log(`👛 Wallets: POST /api/wallets`);
|
|
212
666
|
});
|
|
213
667
|
return app;
|
|
214
668
|
}
|
|
669
|
+
// Auto-start server
|
|
670
|
+
startServer();
|
|
215
671
|
//# sourceMappingURL=server.js.map
|