@xpr-agents/openclaw 0.3.2 → 0.4.1

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.
Files changed (53) hide show
  1. package/README.md +153 -29
  2. package/openclaw.plugin.json +17 -2
  3. package/package.json +7 -4
  4. package/skills/code-sandbox/SKILL.md +30 -0
  5. package/skills/code-sandbox/skill.json +13 -0
  6. package/skills/code-sandbox/src/index.ts +212 -0
  7. package/skills/creative/SKILL.md +32 -0
  8. package/skills/creative/skill.json +13 -0
  9. package/skills/creative/src/index.ts +679 -0
  10. package/skills/defi/SKILL.md +123 -0
  11. package/skills/defi/dist/index.js +1 -0
  12. package/skills/defi/skill.json +44 -0
  13. package/skills/defi/src/index.ts +1788 -0
  14. package/skills/defi/test-read.mjs +281 -0
  15. package/skills/governance/SKILL.md +69 -0
  16. package/skills/governance/dist/index.js +632 -0
  17. package/skills/governance/skill.json +21 -0
  18. package/skills/governance/src/index.ts +656 -0
  19. package/skills/governance/test-read.mjs +176 -0
  20. package/skills/lending/SKILL.md +63 -0
  21. package/skills/lending/dist/index.js +1039 -0
  22. package/skills/lending/skill.json +29 -0
  23. package/skills/lending/src/index.ts +1105 -0
  24. package/skills/lending/test-read.mjs +156 -0
  25. package/skills/nft/SKILL.md +95 -0
  26. package/skills/nft/dist/index.js +4 -10
  27. package/skills/nft/skill.json +37 -0
  28. package/skills/nft/src/index.ts +1539 -0
  29. package/skills/shellbook/SKILL.md +59 -0
  30. package/skills/shellbook/skill.json +29 -0
  31. package/skills/shellbook/src/index.ts +391 -0
  32. package/skills/shellbook/tsconfig.json +14 -0
  33. package/skills/smart-contracts/SKILL.md +128 -0
  34. package/skills/smart-contracts/skill.json +25 -0
  35. package/skills/smart-contracts/src/index.ts +1327 -0
  36. package/skills/smart-contracts/tsconfig.json +14 -0
  37. package/skills/structured-data/SKILL.md +36 -0
  38. package/skills/structured-data/dist/index.js +501 -0
  39. package/skills/structured-data/skill.json +13 -0
  40. package/skills/structured-data/src/index.ts +597 -0
  41. package/skills/tax/SKILL.md +109 -0
  42. package/skills/tax/dist/index.js +216 -32
  43. package/skills/tax/skill.json +20 -0
  44. package/skills/tax/src/index.ts +1985 -0
  45. package/skills/web-scraping/SKILL.md +29 -0
  46. package/skills/web-scraping/dist/index.js +311 -0
  47. package/skills/web-scraping/skill.json +13 -0
  48. package/skills/web-scraping/src/index.ts +371 -0
  49. package/skills/xmd/SKILL.md +52 -0
  50. package/skills/xmd/dist/index.js +596 -0
  51. package/skills/xmd/skill.json +22 -0
  52. package/skills/xmd/src/index.ts +635 -0
  53. package/skills/xmd/test-read.mjs +178 -0
@@ -0,0 +1,596 @@
1
+ "use strict";
2
+ /**
3
+ * XMD Skill — Metal Dollar stablecoin (xmd.token / xmd.treasury)
4
+ *
5
+ * Read-only tools use fetch-based RPC helpers (no signing).
6
+ * Write tools create a session from env vars for signing transactions.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.default = xmdSkill;
43
+ // ── Constants ────────────────────────────────────
44
+ const XMD_TOKEN = 'xmd.token';
45
+ const XMD_TREASURY = 'xmd.treasury';
46
+ const XMD_SYMBOL = 'XMD';
47
+ const XMD_PRECISION = 6;
48
+ const ORACLE_CONTRACT = 'oracles';
49
+ const MAINNET_RPC = 'https://xpr-mainnet-rpc.saltant.io';
50
+ // ── RPC Helper ───────────────────────────────────
51
+ const RPC_TIMEOUT = 15000;
52
+ async function rpcPost(endpoint, path, body) {
53
+ const controller = new AbortController();
54
+ const timer = setTimeout(() => controller.abort(), RPC_TIMEOUT);
55
+ try {
56
+ const resp = await fetch(`${endpoint}${path}`, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify(body),
60
+ signal: controller.signal,
61
+ });
62
+ if (!resp.ok) {
63
+ const text = await resp.text().catch(() => '');
64
+ throw new Error(`RPC ${path} failed (${resp.status}): ${text.slice(0, 200)}`);
65
+ }
66
+ return await resp.json();
67
+ }
68
+ finally {
69
+ clearTimeout(timer);
70
+ }
71
+ }
72
+ async function getTableRows(endpoint, opts) {
73
+ const result = await rpcPost(endpoint, '/v1/chain/get_table_rows', {
74
+ json: opts.json !== false,
75
+ code: opts.code,
76
+ scope: opts.scope,
77
+ table: opts.table,
78
+ lower_bound: opts.lower_bound,
79
+ upper_bound: opts.upper_bound,
80
+ limit: opts.limit || 100,
81
+ key_type: opts.key_type,
82
+ });
83
+ return result.rows || [];
84
+ }
85
+ // ── Session Factory ──────────────────────────────
86
+ // Session backed by the proton CLI — agent process never holds a private key.
87
+ let cachedSession = null;
88
+ async function getXmdSession() {
89
+ if (cachedSession)
90
+ return cachedSession;
91
+ const account = process.env.XPR_ACCOUNT;
92
+ const permission = process.env.XPR_PERMISSION || 'active';
93
+ if (!account)
94
+ throw new Error('XPR_ACCOUNT is required for XMD write operations');
95
+ // @ts-ignore — provided by host at runtime; not resolvable when building skills inside the openclaw package
96
+ const { createCliApi } = await Promise.resolve().then(() => __importStar(require('@xpr-agents/openclaw')));
97
+ cachedSession = createCliApi({ account, permission, rpcEndpoint: MAINNET_RPC });
98
+ return cachedSession;
99
+ }
100
+ // ── Helpers ──────────────────────────────────────
101
+ function parseExtSym(sym) {
102
+ if (!sym)
103
+ return null;
104
+ const symStr = sym.sym || '';
105
+ const parts = symStr.split(',');
106
+ if (parts.length !== 2)
107
+ return null;
108
+ return { precision: parseInt(parts[0]) || 0, symbol: parts[1].trim(), contract: sym.contract || '' };
109
+ }
110
+ function formatAsset(amount, precision, symbol) {
111
+ return `${amount.toFixed(precision)} ${symbol}`;
112
+ }
113
+ function parseQuantity(qty) {
114
+ const parts = qty.trim().split(' ');
115
+ if (parts.length !== 2)
116
+ return null;
117
+ const decParts = parts[0].split('.');
118
+ return {
119
+ amount: parseFloat(parts[0]),
120
+ symbol: parts[1],
121
+ precision: decParts.length > 1 ? decParts[1].length : 0,
122
+ };
123
+ }
124
+ async function getOraclePrice(endpoint, feedIndex) {
125
+ try {
126
+ const rows = await getTableRows(endpoint, {
127
+ code: ORACLE_CONTRACT, scope: ORACLE_CONTRACT, table: 'data',
128
+ lower_bound: feedIndex, upper_bound: feedIndex, limit: 1,
129
+ });
130
+ if (rows.length === 0)
131
+ return null;
132
+ const row = rows[0];
133
+ const price = row.aggregate?.d_double ? parseFloat(row.aggregate.d_double) : 0;
134
+ const providers = (row.points || []).map((p) => ({
135
+ provider: p.provider,
136
+ price: p.data?.d_double ? parseFloat(p.data.d_double) : 0,
137
+ time: p.time,
138
+ }));
139
+ return { price, providers };
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ async function getOracleFeedName(endpoint, feedIndex) {
146
+ try {
147
+ const rows = await getTableRows(endpoint, {
148
+ code: ORACLE_CONTRACT, scope: ORACLE_CONTRACT, table: 'feeds',
149
+ lower_bound: feedIndex, upper_bound: feedIndex, limit: 1,
150
+ });
151
+ return rows.length > 0 ? rows[0].name || null : null;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ // ── Skill Entry Point ────────────────────────────
158
+ function xmdSkill(api) {
159
+ const config = api.getConfig();
160
+ const rpcEndpoint = MAINNET_RPC;
161
+ // ════════════════════════════════════════════════
162
+ // READ-ONLY TOOLS
163
+ // ════════════════════════════════════════════════
164
+ // ── 1. xmd_get_config ──
165
+ api.registerTool({
166
+ name: 'xmd_get_config',
167
+ description: 'Get XMD treasury global configuration — paused state, fee account, minimum oracle price threshold for minting/redeeming.',
168
+ parameters: {
169
+ type: 'object',
170
+ properties: {},
171
+ },
172
+ handler: async () => {
173
+ try {
174
+ const globals = await getTableRows(rpcEndpoint, {
175
+ code: XMD_TREASURY, scope: XMD_TREASURY, table: 'xmdglobals', limit: 1,
176
+ });
177
+ if (globals.length === 0)
178
+ return { error: 'XMD treasury globals not found' };
179
+ const cfg = globals[0];
180
+ return {
181
+ is_paused: !!cfg.isPaused,
182
+ fee_account: cfg.feeAccount,
183
+ min_oracle_price: parseFloat(cfg.minOraclePrice),
184
+ min_oracle_price_note: 'Collateral oracle price must be >= this value for mint/redeem to proceed',
185
+ contracts: {
186
+ token: XMD_TOKEN,
187
+ treasury: XMD_TREASURY,
188
+ oracles: ORACLE_CONTRACT,
189
+ },
190
+ };
191
+ }
192
+ catch (err) {
193
+ return { error: `Failed to get XMD config: ${err.message}` };
194
+ }
195
+ },
196
+ });
197
+ // ── 2. xmd_list_collateral ──
198
+ api.registerTool({
199
+ name: 'xmd_list_collateral',
200
+ description: 'List all supported XMD collateral tokens with mint/redeem status, fees, oracle prices, max treasury percent, and cumulative mint/redeem volumes.',
201
+ parameters: {
202
+ type: 'object',
203
+ properties: {},
204
+ },
205
+ handler: async () => {
206
+ try {
207
+ const tokens = await getTableRows(rpcEndpoint, {
208
+ code: XMD_TREASURY, scope: XMD_TREASURY, table: 'tokens', limit: 50,
209
+ });
210
+ const collateral = await Promise.all(tokens.map(async (t) => {
211
+ const sym = parseExtSym(t.symbol);
212
+ if (!sym)
213
+ return null;
214
+ // Fetch oracle price
215
+ const oracle = await getOraclePrice(rpcEndpoint, t.oracleIndex);
216
+ const feedName = await getOracleFeedName(rpcEndpoint, t.oracleIndex);
217
+ const amountMinted = parseFloat(t.amountMinted) || 0;
218
+ const amountRedeemed = parseFloat(t.amountRedeemed) || 0;
219
+ return {
220
+ symbol: sym.symbol,
221
+ contract: sym.contract,
222
+ precision: sym.precision,
223
+ is_mint_enabled: !!t.isMintEnabled,
224
+ is_redeem_enabled: !!t.isRedeemEnabled,
225
+ mint_fee_pct: `${(parseFloat(t.mintFee) * 100).toFixed(2)}%`,
226
+ redemption_fee_pct: `${(parseFloat(t.redemptionFee) * 100).toFixed(2)}%`,
227
+ max_treasury_pct: `${parseFloat(t.maxTreasuryPercent).toFixed(0)}%`,
228
+ oracle_feed_index: t.oracleIndex,
229
+ oracle_feed_name: feedName,
230
+ oracle_price: oracle?.price ?? null,
231
+ is_liquidation_enabled: !!t.isLiquidationEnabled,
232
+ total_minted: amountMinted.toFixed(2),
233
+ total_redeemed: amountRedeemed.toFixed(2),
234
+ net_outstanding: (amountMinted - amountRedeemed).toFixed(2),
235
+ };
236
+ }));
237
+ return {
238
+ collateral: collateral.filter(Boolean),
239
+ total_types: collateral.filter(Boolean).length,
240
+ note: 'To mint XMD, transfer a supported stablecoin to xmd.treasury with memo "mint". To redeem, transfer XMD with memo "redeem,SYMBOL".',
241
+ };
242
+ }
243
+ catch (err) {
244
+ return { error: `Failed to list collateral: ${err.message}` };
245
+ }
246
+ },
247
+ });
248
+ // ── 3. xmd_get_supply ──
249
+ api.registerTool({
250
+ name: 'xmd_get_supply',
251
+ description: 'Get XMD total circulating supply and token stats.',
252
+ parameters: {
253
+ type: 'object',
254
+ properties: {},
255
+ },
256
+ handler: async () => {
257
+ try {
258
+ const stats = await getTableRows(rpcEndpoint, {
259
+ code: XMD_TOKEN, scope: XMD_SYMBOL, table: 'stat', limit: 1,
260
+ });
261
+ if (stats.length === 0)
262
+ return { error: 'XMD token stats not found' };
263
+ const stat = stats[0];
264
+ const supply = parseQuantity(stat.supply);
265
+ const maxSupply = parseQuantity(stat.max_supply);
266
+ return {
267
+ supply: stat.supply,
268
+ supply_amount: supply?.amount ?? 0,
269
+ max_supply: stat.max_supply,
270
+ max_supply_note: maxSupply?.amount === 0 ? 'Unlimited (max_supply = 0)' : stat.max_supply,
271
+ issuer: stat.issuer,
272
+ precision: XMD_PRECISION,
273
+ token_contract: XMD_TOKEN,
274
+ };
275
+ }
276
+ catch (err) {
277
+ return { error: `Failed to get XMD supply: ${err.message}` };
278
+ }
279
+ },
280
+ });
281
+ // ── 4. xmd_get_balance ──
282
+ api.registerTool({
283
+ name: 'xmd_get_balance',
284
+ description: 'Check an account\'s XMD balance. Returns 0 if the account has no XMD.',
285
+ parameters: {
286
+ type: 'object',
287
+ required: ['account'],
288
+ properties: {
289
+ account: { type: 'string', description: 'XPR Network account name' },
290
+ },
291
+ },
292
+ handler: async ({ account }) => {
293
+ if (!account)
294
+ return { error: 'account is required' };
295
+ try {
296
+ const rows = await getTableRows(rpcEndpoint, {
297
+ code: XMD_TOKEN, scope: account, table: 'accounts', limit: 10,
298
+ });
299
+ const xmdRow = rows.find((r) => {
300
+ const parsed = parseQuantity(r.balance);
301
+ return parsed?.symbol === XMD_SYMBOL;
302
+ });
303
+ const balance = xmdRow ? parseQuantity(xmdRow.balance) : null;
304
+ return {
305
+ account,
306
+ balance: balance ? xmdRow.balance : formatAsset(0, XMD_PRECISION, XMD_SYMBOL),
307
+ amount: balance?.amount ?? 0,
308
+ };
309
+ }
310
+ catch (err) {
311
+ return { error: `Failed to get balance: ${err.message}` };
312
+ }
313
+ },
314
+ });
315
+ // ── 5. xmd_get_treasury_reserves ──
316
+ api.registerTool({
317
+ name: 'xmd_get_treasury_reserves',
318
+ description: 'Get current stablecoin reserves held by the XMD treasury, with USD valuations per collateral type and overall collateralization ratio.',
319
+ parameters: {
320
+ type: 'object',
321
+ properties: {},
322
+ },
323
+ handler: async () => {
324
+ try {
325
+ // Get supported tokens config
326
+ const tokens = await getTableRows(rpcEndpoint, {
327
+ code: XMD_TREASURY, scope: XMD_TREASURY, table: 'tokens', limit: 50,
328
+ });
329
+ // Get XMD supply
330
+ const stats = await getTableRows(rpcEndpoint, {
331
+ code: XMD_TOKEN, scope: XMD_SYMBOL, table: 'stat', limit: 1,
332
+ });
333
+ const xmdSupply = stats.length > 0 ? parseQuantity(stats[0].supply)?.amount || 0 : 0;
334
+ // For each collateral token, fetch treasury balance and oracle price
335
+ let totalReservesUsd = 0;
336
+ const reserves = await Promise.all(tokens.map(async (t) => {
337
+ const sym = parseExtSym(t.symbol);
338
+ if (!sym)
339
+ return null;
340
+ // Fetch treasury's balance of this token
341
+ const balRows = await getTableRows(rpcEndpoint, {
342
+ code: sym.contract, scope: XMD_TREASURY, table: 'accounts', limit: 50,
343
+ });
344
+ const balRow = balRows.find((r) => {
345
+ const p = parseQuantity(r.balance);
346
+ return p?.symbol === sym.symbol;
347
+ });
348
+ const balance = balRow ? parseQuantity(balRow.balance) : null;
349
+ const balAmount = balance?.amount ?? 0;
350
+ // Fetch oracle price
351
+ const oracle = await getOraclePrice(rpcEndpoint, t.oracleIndex);
352
+ const price = oracle?.price ?? 1.0;
353
+ const usdValue = balAmount * price;
354
+ totalReservesUsd += usdValue;
355
+ // Calculate this token's share of treasury
356
+ const treasuryPct = xmdSupply > 0 ? (usdValue / xmdSupply * 100) : 0;
357
+ return {
358
+ symbol: sym.symbol,
359
+ contract: sym.contract,
360
+ balance: balRow?.balance || formatAsset(0, sym.precision, sym.symbol),
361
+ balance_amount: balAmount,
362
+ oracle_price: price,
363
+ usd_value: usdValue.toFixed(2),
364
+ treasury_share_pct: `${treasuryPct.toFixed(1)}%`,
365
+ max_treasury_pct: `${parseFloat(t.maxTreasuryPercent).toFixed(0)}%`,
366
+ };
367
+ }));
368
+ const collateralizationRatio = xmdSupply > 0
369
+ ? (totalReservesUsd / xmdSupply * 100)
370
+ : 0;
371
+ return {
372
+ reserves: reserves.filter(Boolean),
373
+ total_reserves_usd: `$${totalReservesUsd.toLocaleString('en-US', { maximumFractionDigits: 2 })}`,
374
+ total_reserves_raw: totalReservesUsd,
375
+ xmd_supply: formatAsset(xmdSupply, XMD_PRECISION, XMD_SYMBOL),
376
+ xmd_supply_raw: xmdSupply,
377
+ collateralization_ratio_pct: `${collateralizationRatio.toFixed(2)}%`,
378
+ is_fully_collateralized: collateralizationRatio >= 100,
379
+ };
380
+ }
381
+ catch (err) {
382
+ return { error: `Failed to get treasury reserves: ${err.message}` };
383
+ }
384
+ },
385
+ });
386
+ // ── 6. xmd_get_oracle_price ──
387
+ api.registerTool({
388
+ name: 'xmd_get_oracle_price',
389
+ description: 'Get the current oracle price for an XMD collateral token (e.g. XUSDC, XPAX, XPYUSD, MPD). Shows aggregate price and individual provider prices.',
390
+ parameters: {
391
+ type: 'object',
392
+ required: ['symbol'],
393
+ properties: {
394
+ symbol: { type: 'string', description: 'Collateral token symbol (XUSDC, XPAX, XPYUSD, or MPD)' },
395
+ },
396
+ },
397
+ handler: async ({ symbol }) => {
398
+ if (!symbol)
399
+ return { error: 'symbol is required (XUSDC, XPAX, XPYUSD, or MPD)' };
400
+ const sym = symbol.toUpperCase();
401
+ try {
402
+ // Find the token in treasury config to get oracle index
403
+ const tokens = await getTableRows(rpcEndpoint, {
404
+ code: XMD_TREASURY, scope: XMD_TREASURY, table: 'tokens', limit: 50,
405
+ });
406
+ const token = tokens.find((t) => {
407
+ const parsed = parseExtSym(t.symbol);
408
+ return parsed?.symbol === sym;
409
+ });
410
+ if (!token) {
411
+ const available = tokens.map((t) => parseExtSym(t.symbol)?.symbol).filter(Boolean);
412
+ return { error: `Token "${sym}" not found. Available: ${available.join(', ')}` };
413
+ }
414
+ const feedName = await getOracleFeedName(rpcEndpoint, token.oracleIndex);
415
+ const oracle = await getOraclePrice(rpcEndpoint, token.oracleIndex);
416
+ if (!oracle)
417
+ return { error: `Oracle data not available for feed index ${token.oracleIndex}` };
418
+ return {
419
+ symbol: sym,
420
+ oracle_feed_index: token.oracleIndex,
421
+ oracle_feed_name: feedName,
422
+ aggregate_price: oracle.price,
423
+ providers: oracle.providers.map((p) => ({
424
+ provider: p.provider,
425
+ price: p.price,
426
+ last_updated: p.time,
427
+ })),
428
+ provider_count: oracle.providers.length,
429
+ };
430
+ }
431
+ catch (err) {
432
+ return { error: `Failed to get oracle price: ${err.message}` };
433
+ }
434
+ },
435
+ });
436
+ // ════════════════════════════════════════════════
437
+ // WRITE TOOLS (require confirmation)
438
+ // ════════════════════════════════════════════════
439
+ // ── 7. xmd_mint ──
440
+ api.registerTool({
441
+ name: 'xmd_mint',
442
+ description: 'Mint XMD by depositing a supported stablecoin (XUSDC, XPAX, XPYUSD, or MPD). Transfers the stablecoin to xmd.treasury with memo "mint". You receive XMD at the current oracle rate.',
443
+ parameters: {
444
+ type: 'object',
445
+ required: ['collateral_symbol', 'amount', 'confirmed'],
446
+ properties: {
447
+ collateral_symbol: { type: 'string', description: 'Collateral token symbol: XUSDC, XPAX, XPYUSD, or MPD' },
448
+ amount: { type: 'number', description: 'Amount of collateral to deposit (e.g. 100.0 for 100 XUSDC)' },
449
+ confirmed: { type: 'boolean', description: 'Must be true to proceed' },
450
+ },
451
+ },
452
+ handler: async ({ collateral_symbol, amount, confirmed }) => {
453
+ if (!confirmed) {
454
+ return {
455
+ error: 'Confirmation required. Set confirmed=true to mint XMD.',
456
+ collateral_symbol, amount,
457
+ };
458
+ }
459
+ if (!collateral_symbol)
460
+ return { error: 'collateral_symbol is required (XUSDC, XPAX, XPYUSD, or MPD)' };
461
+ if (!amount || amount <= 0)
462
+ return { error: 'amount must be positive' };
463
+ const sym = collateral_symbol.toUpperCase();
464
+ try {
465
+ // Look up token config
466
+ const tokens = await getTableRows(rpcEndpoint, {
467
+ code: XMD_TREASURY, scope: XMD_TREASURY, table: 'tokens', limit: 50,
468
+ });
469
+ const token = tokens.find((t) => {
470
+ const parsed = parseExtSym(t.symbol);
471
+ return parsed?.symbol === sym;
472
+ });
473
+ if (!token) {
474
+ const available = tokens.map((t) => parseExtSym(t.symbol)?.symbol).filter(Boolean);
475
+ return { error: `Token "${sym}" not supported. Available: ${available.join(', ')}` };
476
+ }
477
+ if (!token.isMintEnabled) {
478
+ return { error: `Minting with ${sym} is currently disabled` };
479
+ }
480
+ const parsed = parseExtSym(token.symbol);
481
+ if (!parsed)
482
+ return { error: 'Could not parse token symbol' };
483
+ // Check treasury is not paused
484
+ const globals = await getTableRows(rpcEndpoint, {
485
+ code: XMD_TREASURY, scope: XMD_TREASURY, table: 'xmdglobals', limit: 1,
486
+ });
487
+ if (globals.length > 0 && globals[0].isPaused) {
488
+ return { error: 'XMD treasury is currently paused' };
489
+ }
490
+ const quantity = formatAsset(amount, parsed.precision, parsed.symbol);
491
+ const { api: eosApi, account, permission } = await getXmdSession();
492
+ const result = await eosApi.transact({
493
+ actions: [{
494
+ account: parsed.contract,
495
+ name: 'transfer',
496
+ authorization: [{ actor: account, permission }],
497
+ data: {
498
+ from: account,
499
+ to: XMD_TREASURY,
500
+ quantity,
501
+ memo: 'mint',
502
+ },
503
+ }],
504
+ }, { blocksBehind: 3, expireSeconds: 30 });
505
+ return {
506
+ transaction_id: result.transaction_id || result.processed?.id,
507
+ action: 'mint',
508
+ deposited: quantity,
509
+ deposited_contract: parsed.contract,
510
+ account,
511
+ note: `Deposited ${quantity} to mint XMD at oracle rate. XMD will be sent to your account.`,
512
+ };
513
+ }
514
+ catch (err) {
515
+ return { error: `Failed to mint XMD: ${err.message}` };
516
+ }
517
+ },
518
+ });
519
+ // ── 8. xmd_redeem ──
520
+ api.registerTool({
521
+ name: 'xmd_redeem',
522
+ description: 'Redeem XMD for a supported stablecoin (XUSDC, XPAX, XPYUSD, or MPD). Transfers XMD to xmd.treasury with memo "redeem,SYMBOL". You receive the stablecoin at the current oracle rate.',
523
+ parameters: {
524
+ type: 'object',
525
+ required: ['redeem_for', 'amount', 'confirmed'],
526
+ properties: {
527
+ redeem_for: { type: 'string', description: 'Which stablecoin to receive: XUSDC, XPAX, XPYUSD, or MPD' },
528
+ amount: { type: 'number', description: 'Amount of XMD to redeem (e.g. 100.0)' },
529
+ confirmed: { type: 'boolean', description: 'Must be true to proceed' },
530
+ },
531
+ },
532
+ handler: async ({ redeem_for, amount, confirmed }) => {
533
+ if (!confirmed) {
534
+ return {
535
+ error: 'Confirmation required. Set confirmed=true to redeem XMD.',
536
+ redeem_for, amount,
537
+ };
538
+ }
539
+ if (!redeem_for)
540
+ return { error: 'redeem_for is required (XUSDC, XPAX, XPYUSD, or MPD)' };
541
+ if (!amount || amount <= 0)
542
+ return { error: 'amount must be positive' };
543
+ const sym = redeem_for.toUpperCase();
544
+ try {
545
+ // Validate the target collateral exists and redeem is enabled
546
+ const tokens = await getTableRows(rpcEndpoint, {
547
+ code: XMD_TREASURY, scope: XMD_TREASURY, table: 'tokens', limit: 50,
548
+ });
549
+ const token = tokens.find((t) => {
550
+ const parsed = parseExtSym(t.symbol);
551
+ return parsed?.symbol === sym;
552
+ });
553
+ if (!token) {
554
+ const available = tokens.map((t) => parseExtSym(t.symbol)?.symbol).filter(Boolean);
555
+ return { error: `Token "${sym}" not supported. Available: ${available.join(', ')}` };
556
+ }
557
+ if (!token.isRedeemEnabled) {
558
+ return { error: `Redeeming for ${sym} is currently disabled` };
559
+ }
560
+ // Check treasury is not paused
561
+ const globals = await getTableRows(rpcEndpoint, {
562
+ code: XMD_TREASURY, scope: XMD_TREASURY, table: 'xmdglobals', limit: 1,
563
+ });
564
+ if (globals.length > 0 && globals[0].isPaused) {
565
+ return { error: 'XMD treasury is currently paused' };
566
+ }
567
+ const quantity = formatAsset(amount, XMD_PRECISION, XMD_SYMBOL);
568
+ const { api: eosApi, account, permission } = await getXmdSession();
569
+ const result = await eosApi.transact({
570
+ actions: [{
571
+ account: XMD_TOKEN,
572
+ name: 'transfer',
573
+ authorization: [{ actor: account, permission }],
574
+ data: {
575
+ from: account,
576
+ to: XMD_TREASURY,
577
+ quantity,
578
+ memo: `redeem,${sym}`,
579
+ },
580
+ }],
581
+ }, { blocksBehind: 3, expireSeconds: 30 });
582
+ return {
583
+ transaction_id: result.transaction_id || result.processed?.id,
584
+ action: 'redeem',
585
+ redeemed: quantity,
586
+ redeem_for: sym,
587
+ account,
588
+ note: `Sent ${quantity} to treasury to redeem for ${sym} at oracle rate.`,
589
+ };
590
+ }
591
+ catch (err) {
592
+ return { error: `Failed to redeem XMD: ${err.message}` };
593
+ }
594
+ },
595
+ });
596
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "xmd",
3
+ "version": "1.0.0",
4
+ "description": "Metal Dollar (XMD) stablecoin — mint, redeem, supply analytics, collateral reserves, oracle prices",
5
+ "author": "xpr-agents",
6
+ "category": "defi",
7
+ "tags": ["xmd", "stablecoin", "metal-dollar", "mint", "redeem", "treasury", "collateral"],
8
+ "capabilities": ["xmd-stablecoin", "xmd-analytics"],
9
+ "tools": [
10
+ "xmd_get_config",
11
+ "xmd_list_collateral",
12
+ "xmd_get_supply",
13
+ "xmd_get_balance",
14
+ "xmd_get_treasury_reserves",
15
+ "xmd_get_oracle_price",
16
+ "xmd_mint",
17
+ "xmd_redeem"
18
+ ],
19
+ "requires": {
20
+ "env": []
21
+ }
22
+ }