shogun-core 0.0.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 (60) hide show
  1. package/README.md +71 -0
  2. package/dist/auth/credentialAuth.js +154 -0
  3. package/dist/auth/metamaskAuth.js +264 -0
  4. package/dist/auth/webauthnAuth.js +267 -0
  5. package/dist/config.js +39 -0
  6. package/dist/connector/metamask.js +262 -0
  7. package/dist/events.js +12 -0
  8. package/dist/gun/auth.js +523 -0
  9. package/dist/gun/errors.js +66 -0
  10. package/dist/gun/gun.js +331 -0
  11. package/dist/index.js +440 -0
  12. package/dist/mom/MOMClient.js +1253 -0
  13. package/dist/stealth/stealth.js +289 -0
  14. package/dist/storage/storage.js +93 -0
  15. package/dist/types/auth/credentialAuth.d.ts +56 -0
  16. package/dist/types/auth/metamaskAuth.d.ts +74 -0
  17. package/dist/types/auth/webauthnAuth.d.ts +83 -0
  18. package/dist/types/auth.js +1 -0
  19. package/dist/types/config.d.ts +39 -0
  20. package/dist/types/connector/metamask.d.ts +112 -0
  21. package/dist/types/events.d.ts +27 -0
  22. package/dist/types/gun/auth.d.ts +219 -0
  23. package/dist/types/gun/errors.d.ts +42 -0
  24. package/dist/types/gun/gun.d.ts +124 -0
  25. package/dist/types/gun.js +4 -0
  26. package/dist/types/index.d.ts +173 -0
  27. package/dist/types/mom/MOMClient.d.ts +217 -0
  28. package/dist/types/mom.js +29 -0
  29. package/dist/types/shogun.js +1 -0
  30. package/dist/types/stealth/stealth.d.ts +67 -0
  31. package/dist/types/storage/storage.d.ts +11 -0
  32. package/dist/types/token.js +1 -0
  33. package/dist/types/types/auth.d.ts +47 -0
  34. package/dist/types/types/gun.d.ts +73 -0
  35. package/dist/types/types/mom.d.ts +147 -0
  36. package/dist/types/types/shogun.d.ts +90 -0
  37. package/dist/types/types/token.d.ts +12 -0
  38. package/dist/types/utils/eventEmitter.d.ts +9 -0
  39. package/dist/types/utils/logger.d.ts +24 -0
  40. package/dist/types/utils/storageMock.d.ts +12 -0
  41. package/dist/types/utils/utility.d.ts +20 -0
  42. package/dist/types/utils/wait.d.ts +24 -0
  43. package/dist/types/wallet/gunWallet.d.ts +14 -0
  44. package/dist/types/wallet/hdWallet.d.ts +154 -0
  45. package/dist/types/wallet/walletManager-old.d.ts +70 -0
  46. package/dist/types/wallet/walletManager.d.ts +188 -0
  47. package/dist/types/webauthn/webauthn-gun.d.ts +1 -0
  48. package/dist/types/webauthn/webauthn.d.ts +52 -0
  49. package/dist/utils/eventEmitter.js +29 -0
  50. package/dist/utils/logger.js +39 -0
  51. package/dist/utils/storageMock.js +27 -0
  52. package/dist/utils/utility.js +32 -0
  53. package/dist/utils/wait.js +78 -0
  54. package/dist/wallet/gunWallet.js +14 -0
  55. package/dist/wallet/hdWallet.js +619 -0
  56. package/dist/wallet/walletManager-old.js +473 -0
  57. package/dist/wallet/walletManager.js +1226 -0
  58. package/dist/webauthn/webauthn-gun.js +115 -0
  59. package/dist/webauthn/webauthn.js +313 -0
  60. package/package.json +48 -0
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Shogun SDK
2
+
3
+ ## Overview
4
+
5
+ Welcome to the Shogun SDK! This powerful and user-friendly SDK is designed to simplify decentralized authentication and wallet management. With support for various authentication methods including standard username/password, MetaMask, and WebAuthn, Shogun SDK integrates seamlessly with GunDB for decentralized user authentication. Whether you're building a new application or enhancing an existing one, Shogun SDK provides the tools you need to manage user authentication and crypto wallets efficiently.
6
+
7
+ ## Key Features
8
+
9
+ - **Multi-layer Authentication**: Supports username/password, MetaMask, and WebAuthn.
10
+ - **Wallet Management**: Easily manage crypto wallets, mnemonics, and keys.
11
+ - **GunDB Integration**: Decentralized user authentication with GunDB.
12
+ - **Stealth Addresses**: Create and manage stealth addresses for enhanced privacy.
13
+ - **Storage Solutions**: Simple key-value storage with support for localStorage.
14
+
15
+ ## Installazione
16
+
17
+ ```bash
18
+ npm install @shogun/shogun-core
19
+ # o
20
+ yarn add @shogun/shogun-core
21
+ ```
22
+
23
+ ## Esempio Base d'Uso
24
+
25
+ ```typescript
26
+ import { ShogunSDK } from '@shogun/shogun-core';
27
+
28
+ // Inizializzazione
29
+ const shogun = new ShogunSDK({
30
+ peers: ['https://your-gun-peer.com/gun'],
31
+ localStorage: true
32
+ });
33
+
34
+ // Autenticazione con username/password
35
+ const loginResult = await shogun.login('username', 'password');
36
+
37
+ // Oppure con MetaMask
38
+ const metaMaskLoginResult = await shogun.loginWithMetaMask('ethereumAddress');
39
+
40
+ // Oppure con WebAuthn
41
+ const webAuthnLoginResult = await shogun.loginWithWebAuthn('username');
42
+ ```
43
+
44
+ ## Documentazione Completa
45
+
46
+ Shogun SDK include una documentazione tecnica completa generata con TSDoc:
47
+
48
+ - **Documentazione locale**: Visualizza la documentazione aprendo `./docs/index.html`
49
+ - **Classi principali**: Consulta `./docs/classes/` per dettagli sulle singole classi
50
+ - **Interfacce**: Consulta `./docs/interfaces/` per dettagli sulle interfacce
51
+
52
+ ## Requisiti di Sistema e Compatibilità
53
+
54
+ - **Browser moderni** con supporto per WebAuthn (Chrome, Firefox, Edge, Safari)
55
+ - **Node.js** 14 o superiore
56
+ - Compatibile con **ethers.js** v6
57
+
58
+ ## Contribuire
59
+
60
+ Le contribuzioni sono benvenute! Se desideri contribuire al progetto, per favore:
61
+
62
+ 1. Forka il repository
63
+ 2. Crea un branch per la tua feature (`git checkout -b feature/amazing-feature`)
64
+ 3. Committa i tuoi cambiamenti (`git commit -m 'Aggiunta nuova feature'`)
65
+ 4. Effettua il push al branch (`git push origin feature/amazing-feature`)
66
+ 5. Apri una Pull Request
67
+
68
+ ## Licenza
69
+
70
+ MIT
71
+
@@ -0,0 +1,154 @@
1
+ import { logError, logDebug } from "../utils/logger";
2
+ /**
3
+ * Classe per l'autenticazione con username e password
4
+ * Livello più alto di astrazione per gestire l'autenticazione utente
5
+ */
6
+ export class UserAuth {
7
+ constructor(gundb, gun, storage) {
8
+ this.gundb = gundb;
9
+ this.gun = gun;
10
+ this.storage = storage;
11
+ this.auth = gundb.auth;
12
+ }
13
+ /**
14
+ * Crea un risultato di autenticazione
15
+ * @param success - Indica se l'autenticazione è riuscita
16
+ * @param data - Dati aggiuntivi
17
+ * @returns Risultato dell'autenticazione
18
+ */
19
+ createAuthResult(success, data = {}) {
20
+ return {
21
+ success,
22
+ ...data,
23
+ };
24
+ }
25
+ /**
26
+ * Metodo principale per l'autenticazione dell'utente
27
+ * @param username - Nome utente
28
+ * @param password - Password
29
+ * @param options - Opzioni avanzate per il login
30
+ * @returns Risultato dell'autenticazione
31
+ */
32
+ async login(username, password, options = {}) {
33
+ // Implementazione che sostituisce sia handleLogin che loginWithCredentials e loginWithExtendedOptions
34
+ logDebug(`Tentativo di login per l'utente: ${username} - timestamp: ${Date.now()}`);
35
+ const retryCount = options.retryCount || 1;
36
+ const timeout = options.timeout || 10000;
37
+ let lastError = null;
38
+ for (let attempt = 1; attempt <= retryCount; attempt++) {
39
+ try {
40
+ logDebug(`Tentativo ${attempt}/${retryCount} per l'utente: ${username}`);
41
+ // Adattiamo la chiamata al nuovo formato di auth.ts
42
+ // Convertiamo le nostre opzioni nel formato AuthOptions
43
+ const authOptions = {
44
+ timeout: timeout,
45
+ storePair: true, // Salviamo la coppia di chiavi per default
46
+ };
47
+ // Tentativo di login utilizzando Auth
48
+ const pub = await this.auth.login({ alias: username, pass: password }, authOptions);
49
+ logDebug(`Login riuscito per l'utente: ${username} - pub: ${pub}`);
50
+ // Callbacks opzionali
51
+ if (options.setUserpub) {
52
+ options.setUserpub(pub);
53
+ }
54
+ if (options.setSignedIn) {
55
+ options.setSignedIn(true);
56
+ }
57
+ return this.createAuthResult(true, { userPub: pub });
58
+ }
59
+ catch (error) {
60
+ lastError = error;
61
+ logDebug(`Errore nel tentativo ${attempt}: ${error instanceof Error ? error.message : "Errore sconosciuto"}`);
62
+ }
63
+ }
64
+ // Tutti i tentativi sono falliti
65
+ const errorMessage = lastError instanceof Error
66
+ ? lastError.message
67
+ : "Errore di autenticazione sconosciuto";
68
+ return this.createAuthResult(false, { error: errorMessage });
69
+ }
70
+ /**
71
+ * Metodo principale per la registrazione di un nuovo utente
72
+ * @param username - Nome utente
73
+ * @param password - Password
74
+ * @param passwordConfirmation - Conferma password
75
+ * @param options - Opzioni avanzate per la registrazione
76
+ * @returns Risultato della registrazione
77
+ */
78
+ async register(username, password, passwordConfirmation, options = {}) {
79
+ // Implementazione che sostituisce sia handleSignUp che registerWithExtendedOptions
80
+ logDebug(`Tentativo di registrazione per l'utente: ${username} - timestamp: ${Date.now()}`);
81
+ // Validazione password
82
+ if (password !== passwordConfirmation) {
83
+ const errorMessage = options.messages?.passwordMismatch || "Le password non corrispondono";
84
+ if (options.setErrorMessage) {
85
+ options.setErrorMessage(errorMessage);
86
+ }
87
+ return this.createAuthResult(false, { error: errorMessage });
88
+ }
89
+ // Verifica esistenza utente
90
+ try {
91
+ const exists = await this.auth.exists(username);
92
+ if (exists && !options.forceResetUser) {
93
+ logDebug(`[0/4] Utente ${username} esiste già - timestamp: ${Date.now()}`);
94
+ const errorMessage = options.messages?.userExists || "Nome utente già in uso";
95
+ if (options.setErrorMessage) {
96
+ options.setErrorMessage(errorMessage);
97
+ }
98
+ return this.createAuthResult(false, { error: errorMessage });
99
+ }
100
+ if (exists && options.forceResetUser) {
101
+ logDebug(`[0/4] Utente ${username} esiste, si procede con reset forzato - timestamp: ${Date.now()}`);
102
+ }
103
+ }
104
+ catch (error) {
105
+ // Se c'è un errore nel controllo, procediamo comunque ma lo logghiamo
106
+ logDebug(`[0/4] Errore durante verifica esistenza utente: ${error instanceof Error ? error.message : "Unknown error"} - timestamp: ${Date.now()}`);
107
+ }
108
+ try {
109
+ // Crea un utente GUN usando Auth
110
+ try {
111
+ logDebug(`[1/4] Chiamata a auth.create per ${username} - timestamp: ${Date.now()}`);
112
+ // Adattiamo la chiamata al nuovo formato di auth.ts
113
+ // Convertiamo le nostre opzioni nel formato AuthOptions
114
+ const authOptions = {
115
+ timeout: options.timeout || 10000,
116
+ storePair: true, // Salviamo la coppia di chiavi per default
117
+ };
118
+ // Passando le credenziali nel formato corretto
119
+ const createResult = await this.auth.create({ alias: username, pass: password }, authOptions);
120
+ logDebug(`[1/4] auth.create completato con successo - pub: ${createResult} - timestamp: ${Date.now()}`);
121
+ // Callbacks opzionali
122
+ if (options.setUserpub) {
123
+ options.setUserpub(createResult);
124
+ }
125
+ if (options.setSignedIn) {
126
+ options.setSignedIn(true);
127
+ }
128
+ return this.createAuthResult(true, { userPub: createResult });
129
+ }
130
+ catch (error) {
131
+ const errorMessage = error instanceof Error
132
+ ? error.message
133
+ : "Errore durante la creazione dell'utente";
134
+ logError(`[1/4] ${errorMessage} - timestamp: ${Date.now()}`);
135
+ if (options.setErrorMessage) {
136
+ options.setErrorMessage(errorMessage);
137
+ }
138
+ return this.createAuthResult(false, { error: errorMessage });
139
+ }
140
+ }
141
+ catch (error) {
142
+ const errorMessage = error instanceof Error
143
+ ? error.message
144
+ : "Errore durante la registrazione";
145
+ logError(`Errore generale durante la registrazione: ${errorMessage} - timestamp: ${Date.now()}`);
146
+ if (options.setErrorMessage) {
147
+ options.setErrorMessage(errorMessage);
148
+ }
149
+ return this.createAuthResult(false, { error: errorMessage });
150
+ }
151
+ }
152
+ }
153
+ // Per compatibilità retroattiva
154
+ export const CredentialAuth = UserAuth;
@@ -0,0 +1,264 @@
1
+ import { Auth } from "../gun/auth";
2
+ import { log, logError, logWarning } from "../utils/logger";
3
+ /**
4
+ * Gestisce l'autenticazione con MetaMask
5
+ */
6
+ export class MetaMaskAuth {
7
+ constructor(gundb, gun, storage, metamask) {
8
+ this.signer = null;
9
+ this.gundb = gundb;
10
+ this.gun = gun;
11
+ this.storage = storage;
12
+ this.auth = new Auth(gun);
13
+ this.metamask = metamask;
14
+ }
15
+ /**
16
+ * Crea un oggetto AuthResult
17
+ * @param success - Se l'operazione è riuscita
18
+ * @param data - Dati aggiuntivi
19
+ * @returns AuthResult
20
+ */
21
+ createAuthResult(success, data = {}) {
22
+ return {
23
+ success,
24
+ ...data,
25
+ };
26
+ }
27
+ /**
28
+ * Normalizza un indirizzo Ethereum
29
+ * @param address - Indirizzo da normalizzare
30
+ * @returns Indirizzo normalizzato
31
+ */
32
+ normalizeEthAddress(address) {
33
+ return address.toLowerCase();
34
+ }
35
+ /**
36
+ * Registra un utente con MetaMask
37
+ * @param address - Indirizzo Ethereum da MetaMask
38
+ * @returns Risultato dell'autenticazione
39
+ */
40
+ /**
41
+ * Ottiene il signer Ethereum
42
+ * @returns Signer Ethereum
43
+ */
44
+ getSigner() {
45
+ return this.signer;
46
+ }
47
+ /**
48
+ * Gestisce il login con MetaMask
49
+ * @param address - Indirizzo Ethereum
50
+ * @returns Risultato del login
51
+ */
52
+ async loginWithMetaMask(address) {
53
+ try {
54
+ log(`Tentativo di login con MetaMask per l'indirizzo: ${address}`);
55
+ if (!this.metamask) {
56
+ return this.createAuthResult(false, {
57
+ error: "MetaMask non supportato",
58
+ });
59
+ }
60
+ // Normalizza l'indirizzo
61
+ const normalizedAddress = this.normalizeEthAddress(address);
62
+ // Ottieni le credenziali precedentemente salvate (se esistono)
63
+ const savedCredentials = await this.getMetaMaskCredentials(normalizedAddress);
64
+ if (savedCredentials) {
65
+ log(`Credenziali MetaMask trovate per l'indirizzo: ${normalizedAddress}`);
66
+ // Usa le credenziali salvate per autenticare
67
+ return this.authenticateWithCredentials(savedCredentials.username, savedCredentials.password);
68
+ }
69
+ log(`Nessuna credenziale MetaMask trovata per l'indirizzo: ${normalizedAddress}, richiedo firma`);
70
+ try {
71
+ // Utilizza il connector MetaMask per generare le credenziali
72
+ const credentials = await this.metamask.generateCredentials(normalizedAddress);
73
+ // Salva le credenziali generate
74
+ this.storage.setItem(`metamask_credentials_${normalizedAddress}`, JSON.stringify(credentials));
75
+ // Autentica l'utente con le credenziali generate
76
+ return this.authenticateWithCredentials(credentials.username, credentials.password);
77
+ }
78
+ catch (signError) {
79
+ log("Errore durante la generazione delle credenziali MetaMask:", signError);
80
+ return this.createAuthResult(false, {
81
+ error: signError.message || "Errore durante la firma con MetaMask",
82
+ });
83
+ }
84
+ }
85
+ catch (error) {
86
+ logError(`Errore durante il login con MetaMask per l'indirizzo: ${address}`, error);
87
+ return this.createAuthResult(false, {
88
+ error: error.message || "Errore sconosciuto",
89
+ });
90
+ }
91
+ }
92
+ /**
93
+ * Registra un utente con credenziali MetaMask
94
+ * @param address - Indirizzo Ethereum
95
+ * @returns Risultato dell'autenticazione
96
+ */
97
+ async registerUserWithMetaMask(address) {
98
+ try {
99
+ log(`Registrazione utente MetaMask con indirizzo: ${address}`);
100
+ // Recupero le credenziali salvate
101
+ const credentialsJson = this.storage.getItem(`metamask_credentials_${address}`);
102
+ if (!credentialsJson) {
103
+ logError(`Credenziali non trovate per l'indirizzo: ${address}`);
104
+ return this.createAuthResult(false, {
105
+ error: "Credenziali MetaMask non trovate",
106
+ });
107
+ }
108
+ const credentials = JSON.parse(credentialsJson);
109
+ log(`Credenziali recuperate per: ${credentials.username}`);
110
+ // Verifica se l'utente esiste già
111
+ const isAvailable = await this.gundb.isUsernameAvailable(credentials.username);
112
+ if (!isAvailable) {
113
+ logWarning(`L'utente MetaMask con username ${credentials.username} esiste già`);
114
+ return this.createAuthResult(false, {
115
+ error: "L'utente MetaMask esiste già",
116
+ });
117
+ }
118
+ log(`Utente non esistente, procedo con la registrazione: ${credentials.username}`);
119
+ // Pulisci eventuali sessioni precedenti
120
+ this.gun.user().leave();
121
+ log("Sessione precedente chiusa");
122
+ // Registra l'utente con GUN
123
+ log(`Tentativo registrazione GUN per: ${credentials.username}`);
124
+ await this.gundb.signUp(credentials.username, credentials.password);
125
+ log(`Utente registrato con successo: ${credentials.username}`);
126
+ // Ottieni la chiave pubblica dell'utente
127
+ const userPub = this.gun.user().is?.pub;
128
+ log(`Chiave pubblica utente: ${userPub}`);
129
+ // Salva i dati MetaMask nel profilo dell'utente
130
+ log("Salvataggio dati MetaMask nel profilo...");
131
+ await this.gun.user().get("profile").get("metamask").put({ address });
132
+ log("Dati MetaMask salvati nel profilo");
133
+ // Crea un'associazione tra indirizzo MetaMask e chiave pubblica
134
+ log(`Creazione associazione indirizzo-pub: metamask_${address}`);
135
+ await this.gun.get(`metamask_${address}`).put({ pub: userPub });
136
+ log("Associazione creata con successo");
137
+ return this.createAuthResult(true, {
138
+ userPub,
139
+ username: credentials.username,
140
+ });
141
+ }
142
+ catch (error) {
143
+ logError("Errore durante la registrazione dell'utente MetaMask:", error);
144
+ return this.createAuthResult(false, {
145
+ error: error.message || "Errore durante la registrazione dell'utente",
146
+ });
147
+ }
148
+ }
149
+ /**
150
+ * Gestisce la registrazione con MetaMask
151
+ * @param address - Indirizzo Ethereum
152
+ * @returns Risultato della registrazione
153
+ */
154
+ async signUpWithMetaMask(address) {
155
+ try {
156
+ log("Tentativo di registrazione con MetaMask:", address);
157
+ if (!this.metamask) {
158
+ logError("MetaMask non supportato");
159
+ return this.createAuthResult(false, {
160
+ error: "MetaMask non supportato",
161
+ });
162
+ }
163
+ // Normalizza l'indirizzo
164
+ const normalizedAddress = this.normalizeEthAddress(address);
165
+ log(`Indirizzo normalizzato: ${normalizedAddress}`);
166
+ try {
167
+ // Utilizza il connector MetaMask per generare le credenziali
168
+ log("Generazione credenziali MetaMask...");
169
+ const credentials = await this.metamask.generateCredentials(normalizedAddress);
170
+ log("Credenziali generate con successo:", credentials.username);
171
+ // Salva le credenziali generate
172
+ this.storage.setItem(`metamask_credentials_${normalizedAddress}`, JSON.stringify(credentials));
173
+ log("Credenziali salvate nel localStorage");
174
+ // Registra l'utente con l'indirizzo normalizzato
175
+ log("Avvio registrazione utente con indirizzo normalizzato...");
176
+ const result = await this.registerUserWithMetaMask(normalizedAddress);
177
+ log("Risultato registrazione:", result);
178
+ if (result.success) {
179
+ log("Registrazione completata con successo per:", normalizedAddress);
180
+ }
181
+ else {
182
+ logError("Registrazione fallita per:", normalizedAddress, result.error);
183
+ }
184
+ return result;
185
+ }
186
+ catch (error) {
187
+ logError("Errore specifico durante la registrazione:", error);
188
+ return this.createAuthResult(false, {
189
+ error: error.message || "Errore nella generazione delle credenziali MetaMask",
190
+ });
191
+ }
192
+ }
193
+ catch (error) {
194
+ logError("Errore generale durante la registrazione con MetaMask:", error);
195
+ return this.createAuthResult(false, {
196
+ error: error.message || "Errore sconosciuto",
197
+ });
198
+ }
199
+ }
200
+ /**
201
+ * Ottiene le credenziali MetaMask salvate per un indirizzo
202
+ * @param address - Indirizzo Ethereum
203
+ * @returns Credenziali o null se non trovate
204
+ */
205
+ async getMetaMaskCredentials(address) {
206
+ try {
207
+ const normalizedAddress = this.normalizeEthAddress(address);
208
+ const storedData = this.storage.getItem(`metamask_credentials_${normalizedAddress}`);
209
+ if (!storedData) {
210
+ const credentials = await this.metamask.generateCredentials(normalizedAddress);
211
+ return {
212
+ username: credentials.username,
213
+ password: credentials.password,
214
+ };
215
+ }
216
+ const credentials = JSON.parse(storedData);
217
+ return {
218
+ username: credentials.username,
219
+ password: credentials.password,
220
+ };
221
+ }
222
+ catch (error) {
223
+ logError("Errore durante il recupero delle credenziali MetaMask:", error);
224
+ return null;
225
+ }
226
+ }
227
+ /**
228
+ * Autentica un utente con username e password
229
+ * @param username - Nome utente
230
+ * @param password - Password
231
+ * @returns Risultato dell'autenticazione
232
+ */
233
+ async authenticateWithCredentials(username, password) {
234
+ try {
235
+ // Esegui login con Gun direttamente
236
+ await new Promise((resolve, reject) => {
237
+ this.gun.user().auth(username, password, (ack) => {
238
+ if (ack.err) {
239
+ reject(new Error(ack.err));
240
+ }
241
+ else {
242
+ resolve();
243
+ }
244
+ });
245
+ });
246
+ // Ottieni la chiave pubblica
247
+ const userPub = this.gun.user().is?.pub;
248
+ if (!userPub) {
249
+ return this.createAuthResult(false, {
250
+ error: "Impossibile ottenere la chiave pubblica dell'utente",
251
+ });
252
+ }
253
+ return this.createAuthResult(true, {
254
+ userPub,
255
+ username
256
+ });
257
+ }
258
+ catch (error) {
259
+ return this.createAuthResult(false, {
260
+ error: error.message || "Errore durante l'autenticazione",
261
+ });
262
+ }
263
+ }
264
+ }