@pasosdejesus/mpdj 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/LICENSE.md +16 -0
- package/SKILL.md +310 -0
- package/dist/SKILL.md +310 -0
- package/dist/abis/PasosDeJesusCredentials.json +1342 -0
- package/dist/blockchain/AddSlearnButton.d.ts +5 -0
- package/dist/blockchain/AddSlearnButton.js +58 -0
- package/dist/blockchain/SlearnInfo.d.ts +22 -0
- package/dist/blockchain/SlearnInfo.js +68 -0
- package/dist/blockchain/admin-credentials.d.ts +29 -0
- package/dist/blockchain/admin-credentials.js +91 -0
- package/dist/blockchain/credential-cache.d.ts +33 -0
- package/dist/blockchain/credential-cache.js +129 -0
- package/dist/blockchain/credential-images.d.ts +30 -0
- package/dist/blockchain/credential-images.js +199 -0
- package/dist/blockchain/credentials.d.ts +66 -0
- package/dist/blockchain/credentials.js +128 -0
- package/dist/blockchain/ecosystem-addresses.d.ts +9 -0
- package/dist/blockchain/ecosystem-addresses.js +23 -0
- package/dist/blockchain/index.d.ts +6 -0
- package/dist/blockchain/index.js +12 -0
- package/dist/ia/adJ.md +181 -0
- package/dist/ia/alineacion_bayes_general.md +60 -0
- package/dist/ia/alineacion_bayes_profecia_mayor20G.md +220 -0
- package/dist/ia/alineacion_servicio.md +49 -0
- package/dist/ia/alineacion_testimonios.md +268 -0
- package/dist/ia/construir_en_adJ_y_celo.md +239 -0
- package/dist/ia/entrenamiento-marcos.jsonl +23 -0
- package/dist/ia/entrenamiento-no-adivinar.jsonl +15 -0
- package/dist/ia/estilo_entrenamiento.jsonl +10 -0
- package/dist/ia/indice_entrenamiento.json +43 -0
- package/dist/ia/m.jsonl +3 -0
- package/dist/ia/manifiesto.md +45 -0
- package/dist/ia/piedra_fundamental/README.md +31 -0
- package/dist/ia/piedra_fundamental/marcos-01.usfm +68 -0
- package/dist/ia/piedra_fundamental/marcos-02.usfm +46 -0
- package/dist/ia/piedra_fundamental/marcos-03.usfm +48 -0
- package/dist/ia/piedra_fundamental/marcos-04.usfm +59 -0
- package/dist/ia/piedra_fundamental/marcos-05.usfm +65 -0
- package/dist/ia/piedra_fundamental/marcos-06.usfm +78 -0
- package/dist/ia/piedra_fundamental/marcos-07.usfm +53 -0
- package/dist/ia/piedra_fundamental/marcos-08.usfm +63 -0
- package/dist/ia/piedra_fundamental/marcos-09.usfm +82 -0
- package/dist/ia/piedra_fundamental/marcos-10.usfm +88 -0
- package/dist/ia/piedra_fundamental/marcos-11.usfm +52 -0
- package/dist/ia/piedra_fundamental/marcos-12.usfm +62 -0
- package/dist/ia/piedra_fundamental/marcos-13.usfm +48 -0
- package/dist/ia/piedra_fundamental/marcos-14.usfm +108 -0
- package/dist/ia/piedra_fundamental/marcos-15.usfm +70 -0
- package/dist/ia/piedra_fundamental/marcos-16.usfm +32 -0
- package/dist/ia/principles.md +35 -0
- package/dist/ia/referencia-marcos.md +152 -0
- package/dist/ia/verificacion_cristologica.json +128 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +159 -0
- package/dist/tasks/adJ.d.ts +1 -0
- package/dist/tasks/adJ.js +37 -0
- package/dist/tasks/ai-verify-core.d.ts +27 -0
- package/dist/tasks/ai-verify-core.js +82 -0
- package/dist/tasks/ai.d.ts +3 -0
- package/dist/tasks/ai.js +96 -0
- package/dist/tasks/credentials.d.ts +4 -0
- package/dist/tasks/credentials.js +627 -0
- package/dist/tasks/principles.d.ts +1 -0
- package/dist/tasks/principles.js +37 -0
- package/dist/tasks/req.d.ts +9 -0
- package/dist/tasks/req.js +1164 -0
- package/dist/tasks/skill.d.ts +1 -0
- package/dist/tasks/skill.js +34 -0
- package/dist/test-utils/blockchain-mocks.d.ts +130 -0
- package/dist/test-utils/blockchain-mocks.js +112 -0
- package/ia/adJ.md +181 -0
- package/ia/alineacion_bayes_general.md +60 -0
- package/ia/alineacion_bayes_profecia_mayor20G.md +220 -0
- package/ia/alineacion_servicio.md +49 -0
- package/ia/alineacion_testimonios.md +268 -0
- package/ia/construir_en_adJ_y_celo.md +239 -0
- package/ia/entrenamiento-marcos.jsonl +23 -0
- package/ia/entrenamiento-no-adivinar.jsonl +15 -0
- package/ia/estilo_entrenamiento.jsonl +10 -0
- package/ia/indice_entrenamiento.json +43 -0
- package/ia/m.jsonl +3 -0
- package/ia/manifiesto.md +45 -0
- package/ia/piedra_fundamental/README.md +31 -0
- package/ia/piedra_fundamental/marcos-01.usfm +68 -0
- package/ia/piedra_fundamental/marcos-02.usfm +46 -0
- package/ia/piedra_fundamental/marcos-03.usfm +48 -0
- package/ia/piedra_fundamental/marcos-04.usfm +59 -0
- package/ia/piedra_fundamental/marcos-05.usfm +65 -0
- package/ia/piedra_fundamental/marcos-06.usfm +78 -0
- package/ia/piedra_fundamental/marcos-07.usfm +53 -0
- package/ia/piedra_fundamental/marcos-08.usfm +63 -0
- package/ia/piedra_fundamental/marcos-09.usfm +82 -0
- package/ia/piedra_fundamental/marcos-10.usfm +88 -0
- package/ia/piedra_fundamental/marcos-11.usfm +52 -0
- package/ia/piedra_fundamental/marcos-12.usfm +62 -0
- package/ia/piedra_fundamental/marcos-13.usfm +48 -0
- package/ia/piedra_fundamental/marcos-14.usfm +108 -0
- package/ia/piedra_fundamental/marcos-15.usfm +70 -0
- package/ia/piedra_fundamental/marcos-16.usfm +32 -0
- package/ia/principles.md +35 -0
- package/ia/referencia-marcos.md +152 -0
- package/ia/verificacion_cristologica.json +128 -0
- package/package.json +73 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// src/blockchain/credential-images.ts
|
|
2
|
+
// SVG composition and PNG generation for credential badges.
|
|
3
|
+
//
|
|
4
|
+
// composeCredentialImage reads user SVG + site logos from publicDir,
|
|
5
|
+
// composes layers (border, icon, logo, lock, star), writes SVG + renders PNG.
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
// ==================== SVG ASSETS (inline defaults) ====================
|
|
12
|
+
const LOCK_SVG_FALLBACK = (color) => `<g transform="translate(452,4) scale(1.8)">
|
|
13
|
+
<rect x="0" y="16" width="48" height="32" rx="4" fill="${color}22" stroke="${color}" stroke-width="2"/>
|
|
14
|
+
<path d="M14 16V10a10 10 0 0 1 20 0v6" fill="none" stroke="${color}" stroke-width="2"/>
|
|
15
|
+
</g>`;
|
|
16
|
+
const STAR_SVG = `<g transform="translate(429,220) scale(1.5)">
|
|
17
|
+
<polygon points="24,0 30,18 48,18 33,30 39,48 24,36 9,48 15,30 0,18 18,18"
|
|
18
|
+
fill="#fbbf24" stroke="#d97706" stroke-width="1"/>
|
|
19
|
+
</g>`;
|
|
20
|
+
const SITE_LOGOS = {
|
|
21
|
+
'learn.tg': `<rect x="0" y="0" width="64" height="64" rx="12" fill="#7c3aed"/>
|
|
22
|
+
<text x="32" y="42" text-anchor="middle" font-size="28" fill="white" font-family="sans-serif" font-weight="bold">LT</text>`,
|
|
23
|
+
'sivel.xyz': `<rect x="0" y="0" width="64" height="64" rx="12" fill="#0891b2"/>
|
|
24
|
+
<text x="32" y="42" text-anchor="middle" font-size="24" fill="white" font-family="sans-serif" font-weight="bold">S3</text>`,
|
|
25
|
+
'stable-sl.pdJ.app': `<rect x="0" y="0" width="64" height="64" rx="12" fill="#059669"/>
|
|
26
|
+
<text x="32" y="42" text-anchor="middle" font-size="24" fill="white" font-family="sans-serif" font-weight="bold">SL</text>`,
|
|
27
|
+
};
|
|
28
|
+
export const TYPE_COLORS = {
|
|
29
|
+
course_completion: '#3b82f6',
|
|
30
|
+
role: '#f59e0b',
|
|
31
|
+
achievement: '#10b981',
|
|
32
|
+
nft: '#8b5cf6',
|
|
33
|
+
};
|
|
34
|
+
export const FONT_FAMILY = "system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
|
|
35
|
+
const SHORT_NAMES = {
|
|
36
|
+
'learn.tg': 'learntg',
|
|
37
|
+
'sivel.xyz': 'sivel',
|
|
38
|
+
'stable-sl.pdJ.app': 'stablesl',
|
|
39
|
+
};
|
|
40
|
+
// ==================== SVG HELPERS ====================
|
|
41
|
+
/**
|
|
42
|
+
* Escape text for safe inclusion in XML/SVG attributes and text content.
|
|
43
|
+
* Handles: & < > " '
|
|
44
|
+
*/
|
|
45
|
+
function xmlEscape(text) {
|
|
46
|
+
return text
|
|
47
|
+
.replace(/&/g, '&')
|
|
48
|
+
.replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>')
|
|
50
|
+
.replace(/"/g, '"')
|
|
51
|
+
.replace(/'/g, ''');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract inner content of user's SVG (between <svg> tags).
|
|
55
|
+
*/
|
|
56
|
+
export function extractSvgInner(svgText) {
|
|
57
|
+
const match = svgText.match(/<svg[^>]*>([\s\S]*)<\/svg>/i);
|
|
58
|
+
if (!match)
|
|
59
|
+
throw new Error('Invalid SVG: no <svg> root found');
|
|
60
|
+
return match[1].trim();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate user's SVG file before composition.
|
|
64
|
+
*/
|
|
65
|
+
export function validateSvg(svgText) {
|
|
66
|
+
if (!/<svg[\s\S]*viewBox\s*=\s*["']0 0 512 512["']/i.test(svgText)) {
|
|
67
|
+
throw new Error('SVG must have viewBox="0 0 512 512"');
|
|
68
|
+
}
|
|
69
|
+
const forbidden = [
|
|
70
|
+
/<script/i,
|
|
71
|
+
/href\s*=\s*["']https?:\/\//i,
|
|
72
|
+
/xlink:href\s*=\s*["']https?:\/\//i,
|
|
73
|
+
/data:image\/svg\+xml/i,
|
|
74
|
+
/foreignObject/i,
|
|
75
|
+
];
|
|
76
|
+
for (const pattern of forbidden) {
|
|
77
|
+
if (pattern.test(svgText)) {
|
|
78
|
+
throw new Error(`SVG contains forbidden content: ${pattern.source}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const inner = extractSvgInner(svgText);
|
|
82
|
+
if (inner.length < 50 || inner.length > 50000) {
|
|
83
|
+
throw new Error(`SVG content size out of range (${inner.length} chars)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ==================== LOCK / PDJ / LOGO ====================
|
|
87
|
+
let lockSvgCache = null;
|
|
88
|
+
let pdjSvgCache = null;
|
|
89
|
+
function getLockSvg(color, publicDir) {
|
|
90
|
+
if (!lockSvgCache) {
|
|
91
|
+
const lockSbtPath = path.join(publicDir, 'img', 'lock-sbt.svg');
|
|
92
|
+
if (fs.existsSync(lockSbtPath)) {
|
|
93
|
+
lockSvgCache = extractSvgInner(fs.readFileSync(lockSbtPath, 'utf-8'));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (lockSvgCache) {
|
|
97
|
+
const colored = lockSvgCache.replace(/#10b981/gi, color);
|
|
98
|
+
return `<g transform="translate(405,4) scale(2.0)">${colored}</g>`;
|
|
99
|
+
}
|
|
100
|
+
return LOCK_SVG_FALLBACK(color);
|
|
101
|
+
}
|
|
102
|
+
function getPdJLogo(color, publicDir) {
|
|
103
|
+
if (!pdjSvgCache) {
|
|
104
|
+
// 1. Try project's public/img/
|
|
105
|
+
const projectPath = path.join(publicDir, 'img', 'PdJ-badge.svg');
|
|
106
|
+
if (fs.existsSync(projectPath)) {
|
|
107
|
+
pdjSvgCache = extractSvgInner(fs.readFileSync(projectPath, 'utf-8'));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// 2. Fallback to @pasosdejesus/m/blockchain/img/
|
|
111
|
+
const mPath = path.join(__dirname, 'img', 'PdJ-badge.svg');
|
|
112
|
+
if (fs.existsSync(mPath)) {
|
|
113
|
+
pdjSvgCache = extractSvgInner(fs.readFileSync(mPath, 'utf-8'));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (pdjSvgCache) {
|
|
118
|
+
const colored = pdjSvgCache.replace(/#currentColor/gi, color);
|
|
119
|
+
return `<g transform="translate(11,11) scale(3.5)" opacity="0.85">${colored}</g>`;
|
|
120
|
+
}
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
function getLogoSvg(svgPath) {
|
|
124
|
+
const svg = fs.readFileSync(svgPath, 'utf-8');
|
|
125
|
+
let w = 256, h = 256;
|
|
126
|
+
const vb = svg.match(/viewBox\s*=\s*["']0\s+0\s+([\d.]+)\s+([\d.]+)/i);
|
|
127
|
+
if (vb) {
|
|
128
|
+
w = parseFloat(vb[1]);
|
|
129
|
+
h = parseFloat(vb[2]);
|
|
130
|
+
}
|
|
131
|
+
const maxDim = Math.max(w, h);
|
|
132
|
+
const scale = maxDim > 0 ? (96 / maxDim).toFixed(4) : '0.375';
|
|
133
|
+
return `<g transform="scale(${scale})">${extractSvgInner(svg)}</g>`;
|
|
134
|
+
}
|
|
135
|
+
function getSiteLogo(site, publicDir) {
|
|
136
|
+
const short = SHORT_NAMES[site] || site;
|
|
137
|
+
// 1. Try project's public/img/ directory
|
|
138
|
+
const projectPath = path.join(publicDir, 'img', `logo-${short}.svg`);
|
|
139
|
+
if (fs.existsSync(projectPath))
|
|
140
|
+
return getLogoSvg(projectPath);
|
|
141
|
+
// 2. Fallback to @pasosdejesus/m/blockchain/img/
|
|
142
|
+
const mPath = path.join(__dirname, 'img', `logo-${short}.svg`);
|
|
143
|
+
if (fs.existsSync(mPath))
|
|
144
|
+
return getLogoSvg(mPath);
|
|
145
|
+
// 3. Last resort: inline default
|
|
146
|
+
return SITE_LOGOS[site] || '';
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Compose credential image by layering user's SVG icon with
|
|
150
|
+
* programmatic elements (border, site logo, lock/star overlays).
|
|
151
|
+
*/
|
|
152
|
+
export async function composeCredentialImage(params) {
|
|
153
|
+
const { userSvgPath, tokenId, site, siteName, type, isSoulbound, isPremium, displayName, publicDir, pngCommand } = params;
|
|
154
|
+
// 1. Read and validate user SVG
|
|
155
|
+
if (!fs.existsSync(userSvgPath)) {
|
|
156
|
+
throw new Error(`SVG file not found: ${userSvgPath}`);
|
|
157
|
+
}
|
|
158
|
+
const rawSvg = fs.readFileSync(userSvgPath, 'utf-8');
|
|
159
|
+
validateSvg(rawSvg);
|
|
160
|
+
const userInner = extractSvgInner(rawSvg);
|
|
161
|
+
const color = TYPE_COLORS[type] || '#64748b';
|
|
162
|
+
const siteLogo = getSiteLogo(site, publicDir);
|
|
163
|
+
if (!siteLogo)
|
|
164
|
+
throw new Error(`Unknown site: ${site}`);
|
|
165
|
+
const generatedDir = path.join(publicDir, 'img', 'credential', 'generated');
|
|
166
|
+
const pngDir = path.join(publicDir, 'img', 'credential');
|
|
167
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
168
|
+
fs.mkdirSync(pngDir, { recursive: true });
|
|
169
|
+
// 2. Compose layers
|
|
170
|
+
const layers = [
|
|
171
|
+
`<rect x="0" y="0" width="512" height="512" rx="28" fill="#ffffff"/>`,
|
|
172
|
+
`<rect x="0" y="0" width="512" height="512" rx="28" fill="none" stroke="${color}" stroke-width="14"/>`,
|
|
173
|
+
getPdJLogo(color, publicDir),
|
|
174
|
+
`<g transform="translate(11,11) scale(0.95)">${userInner}</g>`,
|
|
175
|
+
`<rect x="0" y="464" width="512" height="48" rx="0" fill="${color}" opacity="0.15"/>`,
|
|
176
|
+
`<rect x="0" y="464" width="512" height="3" rx="0" fill="${color}" opacity="0.6"/>`,
|
|
177
|
+
`<g transform="translate(405,364)">${siteLogo}</g>`,
|
|
178
|
+
`<text x="501" y="498" font-family="${FONT_FAMILY}" font-weight="700" font-size="32" fill="${color}" text-anchor="end">${xmlEscape(siteName)}</text>`,
|
|
179
|
+
`<text x="8" y="498" font-family="${FONT_FAMILY}" font-weight="700" font-size="${displayName.length > 20 ? 20 : 36}" fill="${color}">${xmlEscape(displayName)}</text>`,
|
|
180
|
+
];
|
|
181
|
+
if (isSoulbound)
|
|
182
|
+
layers.push(getLockSvg(color, publicDir));
|
|
183
|
+
if (isPremium)
|
|
184
|
+
layers.push(STAR_SVG);
|
|
185
|
+
const composedSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
186
|
+
${layers.join('\n')}
|
|
187
|
+
</svg>
|
|
188
|
+
`;
|
|
189
|
+
// 3. Write composed SVG
|
|
190
|
+
const svgPath = path.join(generatedDir, `${tokenId}.svg`);
|
|
191
|
+
fs.writeFileSync(svgPath, composedSvg, 'utf-8');
|
|
192
|
+
// 4. Convert to PNG
|
|
193
|
+
const pngPath = path.join(pngDir, `${tokenId}.png`);
|
|
194
|
+
execSync(`${pngCommand} "${pngPath}" "${svgPath}"`, { stdio: 'pipe', timeout: 10000 });
|
|
195
|
+
if (!fs.existsSync(pngPath)) {
|
|
196
|
+
throw new Error('PNG conversion failed — check pngCommand');
|
|
197
|
+
}
|
|
198
|
+
return { svgPath: `img/credential/generated/${tokenId}.svg`, pngPath: `img/credential/${tokenId}.png` };
|
|
199
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { PublicClient, WalletClient, Chain, Account } from 'viem';
|
|
2
|
+
/**
|
|
3
|
+
* Get tokenId from courseId (reads from contract).
|
|
4
|
+
*/
|
|
5
|
+
export declare function getTokenIdByCourseId(publicClient: PublicClient, contractAddress: `0x${string}`, courseId: number): Promise<number>;
|
|
6
|
+
/**
|
|
7
|
+
* Check if an address already holds a credential (reads balanceOf from contract).
|
|
8
|
+
* For SBTs, balance > 0 means they have it (always amount=1).
|
|
9
|
+
* For NFTs, balance > 0 means they hold at least one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function hasCredentialOnChain(publicClient: PublicClient, contractAddress: `0x${string}`, account: `0x${string}`, tokenId: number): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Revoke (burn) a credential from a user.
|
|
14
|
+
*/
|
|
15
|
+
export declare function revokeCredential(walletClient: WalletClient, contractAddress: `0x${string}`, userAddress: `0x${string}`, tokenId: number, amount: number): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Generic mint with Celo L2 nonce retry. Handles replacement/underpriced errors.
|
|
18
|
+
*
|
|
19
|
+
* Celo's OP Stack sequencer rejects txs that try to replace pending ones
|
|
20
|
+
* ("replacement transaction underpriced"). This function handles that by:
|
|
21
|
+
*
|
|
22
|
+
* 1. Using blockTag: 'pending' to skip stuck nonces
|
|
23
|
+
* 2. Passing explicit gasPrice from the network
|
|
24
|
+
* 3. Creating fresh clients each attempt (no http() caching)
|
|
25
|
+
* 4. Retrying up to 5 times with 2s delays
|
|
26
|
+
*
|
|
27
|
+
* @param functionName — 'mintCredential' or 'mintCourseCompletion'
|
|
28
|
+
* @param args — the writeContract args array
|
|
29
|
+
* @returns tx hash on success
|
|
30
|
+
* @throws after all retries exhausted or on non-retryable errors
|
|
31
|
+
*/
|
|
32
|
+
export declare function mintWithRetry(params: {
|
|
33
|
+
account: Account;
|
|
34
|
+
rpcUrl: string;
|
|
35
|
+
chain: Chain;
|
|
36
|
+
contractAddress: `0x${string}`;
|
|
37
|
+
functionName: string;
|
|
38
|
+
args: any[];
|
|
39
|
+
maxAttempts?: number;
|
|
40
|
+
}): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Mint a credential by tokenId with Celo L2 nonce handling.
|
|
43
|
+
* Calls mintCredential(address, tokenId, 1).
|
|
44
|
+
*/
|
|
45
|
+
export declare function mintCredentialWithRetry(params: {
|
|
46
|
+
account: Account;
|
|
47
|
+
rpcUrl: string;
|
|
48
|
+
chain: Chain;
|
|
49
|
+
contractAddress: `0x${string}`;
|
|
50
|
+
userAddress: `0x${string}`;
|
|
51
|
+
tokenId: number;
|
|
52
|
+
maxAttempts?: number;
|
|
53
|
+
}): Promise<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Mint a course completion SBT by courseId with Celo L2 nonce handling.
|
|
56
|
+
* Calls mintCourseCompletion(address, courseId).
|
|
57
|
+
*/
|
|
58
|
+
export declare function mintCourseWithRetry(params: {
|
|
59
|
+
account: Account;
|
|
60
|
+
rpcUrl: string;
|
|
61
|
+
chain: Chain;
|
|
62
|
+
contractAddress: `0x${string}`;
|
|
63
|
+
userAddress: `0x${string}`;
|
|
64
|
+
courseId: number;
|
|
65
|
+
maxAttempts?: number;
|
|
66
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/blockchain/credentials.ts
|
|
2
|
+
// Pure functions for PasosDeJesusCredentials contract interactions.
|
|
3
|
+
//
|
|
4
|
+
// All functions receive WalletClient/PublicClient and contractAddress
|
|
5
|
+
// as parameters. The caller wires its own keys, networks, and address.
|
|
6
|
+
import { createPublicClient, createWalletClient, http } from 'viem';
|
|
7
|
+
import pasosDeJesusCredentialsAbi from '../abis/PasosDeJesusCredentials.json' with { type: 'json' };
|
|
8
|
+
/**
|
|
9
|
+
* Get tokenId from courseId (reads from contract).
|
|
10
|
+
*/
|
|
11
|
+
export async function getTokenIdByCourseId(publicClient, contractAddress, courseId) {
|
|
12
|
+
const tokenId = await publicClient.readContract({
|
|
13
|
+
address: contractAddress,
|
|
14
|
+
abi: pasosDeJesusCredentialsAbi,
|
|
15
|
+
functionName: 'courseIdToTokenId',
|
|
16
|
+
args: [BigInt(courseId)],
|
|
17
|
+
});
|
|
18
|
+
return Number(tokenId);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if an address already holds a credential (reads balanceOf from contract).
|
|
22
|
+
* For SBTs, balance > 0 means they have it (always amount=1).
|
|
23
|
+
* For NFTs, balance > 0 means they hold at least one.
|
|
24
|
+
*/
|
|
25
|
+
export async function hasCredentialOnChain(publicClient, contractAddress, account, tokenId) {
|
|
26
|
+
try {
|
|
27
|
+
const balance = await publicClient.readContract({
|
|
28
|
+
address: contractAddress,
|
|
29
|
+
abi: pasosDeJesusCredentialsAbi,
|
|
30
|
+
functionName: 'balanceOf',
|
|
31
|
+
args: [account, BigInt(tokenId)],
|
|
32
|
+
});
|
|
33
|
+
return balance > 0n;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Revoke (burn) a credential from a user.
|
|
41
|
+
*/
|
|
42
|
+
export async function revokeCredential(walletClient, contractAddress, userAddress, tokenId, amount) {
|
|
43
|
+
return await walletClient.writeContract({
|
|
44
|
+
address: contractAddress,
|
|
45
|
+
abi: pasosDeJesusCredentialsAbi,
|
|
46
|
+
functionName: 'revokeCredential',
|
|
47
|
+
args: [userAddress, BigInt(tokenId), BigInt(amount)],
|
|
48
|
+
chain: walletClient.chain,
|
|
49
|
+
account: walletClient.account,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Generic mint with Celo L2 nonce retry. Handles replacement/underpriced errors.
|
|
54
|
+
*
|
|
55
|
+
* Celo's OP Stack sequencer rejects txs that try to replace pending ones
|
|
56
|
+
* ("replacement transaction underpriced"). This function handles that by:
|
|
57
|
+
*
|
|
58
|
+
* 1. Using blockTag: 'pending' to skip stuck nonces
|
|
59
|
+
* 2. Passing explicit gasPrice from the network
|
|
60
|
+
* 3. Creating fresh clients each attempt (no http() caching)
|
|
61
|
+
* 4. Retrying up to 5 times with 2s delays
|
|
62
|
+
*
|
|
63
|
+
* @param functionName — 'mintCredential' or 'mintCourseCompletion'
|
|
64
|
+
* @param args — the writeContract args array
|
|
65
|
+
* @returns tx hash on success
|
|
66
|
+
* @throws after all retries exhausted or on non-retryable errors
|
|
67
|
+
*/
|
|
68
|
+
export async function mintWithRetry(params) {
|
|
69
|
+
const { account, rpcUrl, chain, contractAddress, functionName, args, maxAttempts = 5 } = params;
|
|
70
|
+
let lastError;
|
|
71
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
72
|
+
// Fresh clients each attempt — http() caches nonce, can't reuse
|
|
73
|
+
const pc = createPublicClient({ chain, transport: http(rpcUrl) });
|
|
74
|
+
const wc = createWalletClient({ chain, transport: http(rpcUrl), account });
|
|
75
|
+
const nonce = await pc.getTransactionCount({
|
|
76
|
+
address: account.address,
|
|
77
|
+
blockTag: attempt <= 3 ? 'pending' : 'latest',
|
|
78
|
+
});
|
|
79
|
+
const gasPrice = await pc.getGasPrice();
|
|
80
|
+
try {
|
|
81
|
+
return await wc.writeContract({
|
|
82
|
+
address: contractAddress,
|
|
83
|
+
abi: pasosDeJesusCredentialsAbi,
|
|
84
|
+
functionName,
|
|
85
|
+
args,
|
|
86
|
+
chain,
|
|
87
|
+
account,
|
|
88
|
+
nonce,
|
|
89
|
+
gasPrice,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
lastError = err;
|
|
94
|
+
const msg = err.message || '';
|
|
95
|
+
if (msg.includes('replacement') || msg.includes('underpriced') ||
|
|
96
|
+
msg.includes('Missing or invalid')) {
|
|
97
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw lastError;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Mint a credential by tokenId with Celo L2 nonce handling.
|
|
107
|
+
* Calls mintCredential(address, tokenId, 1).
|
|
108
|
+
*/
|
|
109
|
+
export async function mintCredentialWithRetry(params) {
|
|
110
|
+
const { userAddress, tokenId, ...rest } = params;
|
|
111
|
+
return mintWithRetry({
|
|
112
|
+
...rest,
|
|
113
|
+
functionName: 'mintCredential',
|
|
114
|
+
args: [userAddress, BigInt(tokenId), BigInt(1)],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Mint a course completion SBT by courseId with Celo L2 nonce handling.
|
|
119
|
+
* Calls mintCourseCompletion(address, courseId).
|
|
120
|
+
*/
|
|
121
|
+
export async function mintCourseWithRetry(params) {
|
|
122
|
+
const { userAddress, courseId, ...rest } = params;
|
|
123
|
+
return mintWithRetry({
|
|
124
|
+
...rest,
|
|
125
|
+
functionName: 'mintCourseCompletion',
|
|
126
|
+
args: [userAddress, BigInt(courseId)],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type CeloNetwork = 'celo' | 'celoSepolia';
|
|
2
|
+
export declare function resolveNetwork(): CeloNetwork;
|
|
3
|
+
export declare const SLEARN_ADDRESSES: Record<CeloNetwork, `0x${string}`>;
|
|
4
|
+
export declare const USDT_ADDRESSES: Record<CeloNetwork, `0x${string}`>;
|
|
5
|
+
export declare const SIGNERS: {
|
|
6
|
+
readonly learnTg: `0x${string}`;
|
|
7
|
+
readonly sivel3: `0x${string}`;
|
|
8
|
+
readonly stableSl: `0x${string}`;
|
|
9
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/blockchain/ecosystem-addresses.ts
|
|
2
|
+
// Shared addresses for contracts and EIP-191 signers across the Pasos de Jesús ecosystem.
|
|
3
|
+
// Used by learn.tg, sivel.xyz, stable-sl.pdJ.app.
|
|
4
|
+
export function resolveNetwork() {
|
|
5
|
+
return (process.env.NEXT_PUBLIC_NETWORK === 'celo' ? 'celo' : 'celoSepolia');
|
|
6
|
+
}
|
|
7
|
+
// ── Contracts ────────────────────────────────────────────
|
|
8
|
+
export const SLEARN_ADDRESSES = {
|
|
9
|
+
celo: '0x27fd41Bea85C39254f2B12789eB37a1543152CC1',
|
|
10
|
+
celoSepolia: process.env.NEXT_PUBLIC_SLEARN_ADDRESS,
|
|
11
|
+
};
|
|
12
|
+
export const USDT_ADDRESSES = {
|
|
13
|
+
celo: '0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e',
|
|
14
|
+
celoSepolia: process.env.NEXT_PUBLIC_USDT_ADDRESS,
|
|
15
|
+
};
|
|
16
|
+
// ── EIP-191 Signers ──────────────────────────────────────
|
|
17
|
+
// Addresses used by each project to sign cross-project messages.
|
|
18
|
+
// These are not contracts but EOA wallets whose signatures are verified via EIP-191.
|
|
19
|
+
export const SIGNERS = {
|
|
20
|
+
learnTg: '0x358643bAdcC77Cccb28A319abD439438A57339A7',
|
|
21
|
+
sivel3: '0x9F636E5653b649b44c9375E6E103600AE55aF979',
|
|
22
|
+
stableSl: '0x6B3BC1b55b28380193733a2FD27f2639d92f14be',
|
|
23
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './credentials.js';
|
|
2
|
+
export * from './credential-cache.js';
|
|
3
|
+
export { verifyCredentialMetadataSchema } from './credential-cache.js';
|
|
4
|
+
export * from './ecosystem-addresses.js';
|
|
5
|
+
export { SlearnInfo } from './SlearnInfo.js';
|
|
6
|
+
export { AddSlearnButton } from './AddSlearnButton.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/blockchain/index.ts
|
|
2
|
+
// Server-only modules (credential-images, admin-credentials, deployments)
|
|
3
|
+
// are NOT exported from the barrel to avoid bundling Node.js 'fs' into client builds.
|
|
4
|
+
// Import them directly: import { ... } from '@pasosdejesus/m/blockchain/credential-images'
|
|
5
|
+
// import { ... } from '@pasosdejesus/m/blockchain/admin-credentials'
|
|
6
|
+
// import { ... } from '@pasosdejesus/m/blockchain/deployments'
|
|
7
|
+
export * from './credentials.js';
|
|
8
|
+
export * from './credential-cache.js';
|
|
9
|
+
export { verifyCredentialMetadataSchema } from './credential-cache.js';
|
|
10
|
+
export * from './ecosystem-addresses.js';
|
|
11
|
+
export { SlearnInfo } from './SlearnInfo.js';
|
|
12
|
+
export { AddSlearnButton } from './AddSlearnButton.js';
|
package/dist/ia/adJ.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# OpenBSD/adJ Tips for AI Agents
|
|
2
|
+
|
|
3
|
+
> *"By wisdom a house is built, and by understanding it is established"* (Proverbs 24:3)
|
|
4
|
+
|
|
5
|
+
This document catalogs platform-specific quirks that cause recurring errors
|
|
6
|
+
when AI agents assume GNU/Linux conventions. Add new entries as they surface.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Shell & CLI
|
|
11
|
+
|
|
12
|
+
### grep — BSD grep, not GNU
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
# ❌ GNU-only flags
|
|
16
|
+
grep -P 'pattern' # -P (Perl regex) does not exist
|
|
17
|
+
grep -oP 'pattern' # same
|
|
18
|
+
grep --exclude-dir=... # use --exclude instead
|
|
19
|
+
|
|
20
|
+
# ✅ BSD-compatible
|
|
21
|
+
grep -E 'pattern' # extended regex
|
|
22
|
+
grep -c 'a' file # OK for single file
|
|
23
|
+
# Multi-file: prefer separate invocations or handle exit code 1
|
|
24
|
+
grep -c 'a' file1 && grep -c 'a' file2
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### sed — BSD sed requires backup extension
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
# ❌ GNU
|
|
31
|
+
sed -i 's/foo/bar/' file
|
|
32
|
+
|
|
33
|
+
# ✅ BSD — empty string for no backup
|
|
34
|
+
sed -i '' 's/foo/bar/' file
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### bash — not at /bin/bash
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# ❌
|
|
41
|
+
#!/bin/bash
|
|
42
|
+
|
|
43
|
+
# ✅
|
|
44
|
+
#!/usr/bin/env bash
|
|
45
|
+
# Or: /usr/local/bin/bash
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Shell defaults
|
|
49
|
+
|
|
50
|
+
OpenBSD's default shell is `ksh`, not `bash`. Scripts without shebangs run
|
|
51
|
+
under `ksh`. Use explicit `#!/usr/bin/env bash` for bash features.
|
|
52
|
+
|
|
53
|
+
### Pipe exit codes
|
|
54
|
+
|
|
55
|
+
BSD utilities can produce exit code 1 in pipelines where GNU returns 0.
|
|
56
|
+
Example: `grep -c 'x' file1 file2 | head` — exit 1 if any file has 0 matches.
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
# ❌ May silently fail
|
|
60
|
+
grep -c 'pattern' f1 f2 | head
|
|
61
|
+
|
|
62
|
+
# ✅ Separate invocations or ignore exit code
|
|
63
|
+
grep -c 'pattern' f1; grep -c 'pattern' f2
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## System Administration
|
|
69
|
+
|
|
70
|
+
### Privilege escalation: doas, not sudo
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
# ❌
|
|
74
|
+
sudo command
|
|
75
|
+
|
|
76
|
+
# ✅
|
|
77
|
+
doas command
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Package management: pkg_add, not apt/yum
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
# ❌
|
|
84
|
+
apt install pkg
|
|
85
|
+
yum install pkg
|
|
86
|
+
|
|
87
|
+
# ✅
|
|
88
|
+
doas pkg_add pkgname
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Service management: rcctl, not systemctl
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
# ❌
|
|
95
|
+
systemctl start postgresql
|
|
96
|
+
|
|
97
|
+
# ✅
|
|
98
|
+
doas rcctl start postgresql
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### /proc — minimal on OpenBSD
|
|
102
|
+
|
|
103
|
+
`/proc` exists but only exposes PID and basic info. No `/proc/cpuinfo`,
|
|
104
|
+
`/proc/meminfo`, etc. Use `sysctl` instead:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
# ❌
|
|
108
|
+
cat /proc/cpuinfo
|
|
109
|
+
|
|
110
|
+
# ✅
|
|
111
|
+
sysctl hw.model hw.ncpu
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## PostgreSQL
|
|
117
|
+
|
|
118
|
+
### System user: _postgresql
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
# ❌ — assumes Linux
|
|
122
|
+
PGUSER=postgres
|
|
123
|
+
|
|
124
|
+
# ✅ — OpenBSD
|
|
125
|
+
PGUSER=_postgresql
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Unix sockets by default
|
|
129
|
+
|
|
130
|
+
PostgreSQL on OpenBSD uses Unix sockets by default. Omit `PGHOST` or set it
|
|
131
|
+
to the socket directory:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
PGHOST=/var/www/var/run/postgresql
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
### Package manager: pnpm
|
|
142
|
+
|
|
143
|
+
`pnpm` has better performance than `npm`/`yarn` on OpenBSD. Use it consistently.
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
pnpm install
|
|
147
|
+
pnpm run build
|
|
148
|
+
pnpm test
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Native binaries (napi-rs)
|
|
152
|
+
|
|
153
|
+
Many npm packages with native Rust binaries (via napi-rs) lack prebuilt
|
|
154
|
+
OpenBSD binaries. Fallbacks:
|
|
155
|
+
- `@nomicfoundation/edr` → `@pasosdejesus/edr` (@ethereumjs/vm)
|
|
156
|
+
- General pattern: look for `@pasosdejesus/*` fallback packages
|
|
157
|
+
|
|
158
|
+
### make — BSD make, not GNU
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
# ❌ GNU-specific
|
|
162
|
+
make -j$(nproc)
|
|
163
|
+
|
|
164
|
+
# ✅ BSD
|
|
165
|
+
make -j$(sysctl -n hw.ncpuonline)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## File Paths
|
|
171
|
+
|
|
172
|
+
### No /bin/bash
|
|
173
|
+
|
|
174
|
+
Use `/usr/bin/env bash` or `/usr/local/bin/bash`.
|
|
175
|
+
|
|
176
|
+
### Ports tree
|
|
177
|
+
|
|
178
|
+
OpenBSD packages installed via `pkg_add` go to `/usr/local/`. This means:
|
|
179
|
+
- Config in `/etc/` for base, `/usr/local/etc/` for packages
|
|
180
|
+
- Binaries in `/usr/local/bin/` for packages
|
|
181
|
+
- Libraries in `/usr/local/lib/` for packages
|