@openzeppelin/wizard-stellar 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.
Files changed (109) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +8 -0
  3. package/README.md +57 -0
  4. package/dist/add-pausable.d.ts +4 -0
  5. package/dist/add-pausable.d.ts.map +1 -0
  6. package/dist/add-pausable.js +37 -0
  7. package/dist/add-pausable.js.map +1 -0
  8. package/dist/api.d.ts +22 -0
  9. package/dist/api.d.ts.map +1 -0
  10. package/dist/api.js +9 -0
  11. package/dist/api.js.map +1 -0
  12. package/dist/build-generic.d.ts +9 -0
  13. package/dist/build-generic.d.ts.map +1 -0
  14. package/dist/build-generic.js +15 -0
  15. package/dist/build-generic.js.map +1 -0
  16. package/dist/common-options.d.ts +15 -0
  17. package/dist/common-options.d.ts.map +1 -0
  18. package/dist/common-options.js +31 -0
  19. package/dist/common-options.js.map +1 -0
  20. package/dist/contract.d.ts +91 -0
  21. package/dist/contract.d.ts.map +1 -0
  22. package/dist/contract.js +123 -0
  23. package/dist/contract.js.map +1 -0
  24. package/dist/error.d.ts +8 -0
  25. package/dist/error.d.ts.map +1 -0
  26. package/dist/error.js +11 -0
  27. package/dist/error.js.map +1 -0
  28. package/dist/fungible.d.ts +24 -0
  29. package/dist/fungible.d.ts.map +1 -0
  30. package/dist/fungible.js +264 -0
  31. package/dist/fungible.js.map +1 -0
  32. package/dist/generate/alternatives.d.ts +7 -0
  33. package/dist/generate/alternatives.d.ts.map +1 -0
  34. package/dist/generate/alternatives.js +27 -0
  35. package/dist/generate/alternatives.js.map +1 -0
  36. package/dist/generate/fungible.d.ts +3 -0
  37. package/dist/generate/fungible.d.ts.map +1 -0
  38. package/dist/generate/fungible.js +21 -0
  39. package/dist/generate/fungible.js.map +1 -0
  40. package/dist/generate/sources.d.ts +16 -0
  41. package/dist/generate/sources.d.ts.map +1 -0
  42. package/dist/generate/sources.js +62 -0
  43. package/dist/generate/sources.js.map +1 -0
  44. package/dist/index.d.ts +16 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +24 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/kind.d.ts +4 -0
  49. package/dist/kind.d.ts.map +1 -0
  50. package/dist/kind.js +24 -0
  51. package/dist/kind.js.map +1 -0
  52. package/dist/print.d.ts +3 -0
  53. package/dist/print.d.ts.map +1 -0
  54. package/dist/print.js +257 -0
  55. package/dist/print.js.map +1 -0
  56. package/dist/set-access-control.d.ts +15 -0
  57. package/dist/set-access-control.d.ts.map +1 -0
  58. package/dist/set-access-control.js +67 -0
  59. package/dist/set-access-control.js.map +1 -0
  60. package/dist/set-info.d.ts +10 -0
  61. package/dist/set-info.d.ts.map +1 -0
  62. package/dist/set-info.js +13 -0
  63. package/dist/set-info.js.map +1 -0
  64. package/dist/test.d.ts +2 -0
  65. package/dist/test.d.ts.map +1 -0
  66. package/dist/test.js +21 -0
  67. package/dist/test.js.map +1 -0
  68. package/dist/utils/convert-strings.d.ts +27 -0
  69. package/dist/utils/convert-strings.d.ts.map +1 -0
  70. package/dist/utils/convert-strings.js +81 -0
  71. package/dist/utils/convert-strings.js.map +1 -0
  72. package/dist/utils/define-functions.d.ts +5 -0
  73. package/dist/utils/define-functions.d.ts.map +1 -0
  74. package/dist/utils/define-functions.js +7 -0
  75. package/dist/utils/define-functions.js.map +1 -0
  76. package/dist/utils/format-lines.d.ts +6 -0
  77. package/dist/utils/format-lines.d.ts.map +1 -0
  78. package/dist/utils/format-lines.js +28 -0
  79. package/dist/utils/format-lines.js.map +1 -0
  80. package/dist/utils/sorted-by.d.ts +2 -0
  81. package/dist/utils/sorted-by.d.ts.map +1 -0
  82. package/dist/utils/sorted-by.js +7 -0
  83. package/dist/utils/sorted-by.js.map +1 -0
  84. package/dist/utils/version.d.ts +10 -0
  85. package/dist/utils/version.d.ts.map +1 -0
  86. package/dist/utils/version.js +13 -0
  87. package/dist/utils/version.js.map +1 -0
  88. package/package.json +33 -0
  89. package/src/add-pausable.ts +40 -0
  90. package/src/api.ts +30 -0
  91. package/src/build-generic.ts +19 -0
  92. package/src/common-options.ts +38 -0
  93. package/src/contract.ts +201 -0
  94. package/src/error.ts +7 -0
  95. package/src/fungible.ts +300 -0
  96. package/src/generate/alternatives.ts +32 -0
  97. package/src/generate/fungible.ts +21 -0
  98. package/src/generate/sources.ts +75 -0
  99. package/src/index.ts +24 -0
  100. package/src/kind.ts +26 -0
  101. package/src/print.ts +305 -0
  102. package/src/set-access-control.ts +73 -0
  103. package/src/set-info.ts +17 -0
  104. package/src/test.ts +26 -0
  105. package/src/utils/convert-strings.ts +83 -0
  106. package/src/utils/define-functions.ts +9 -0
  107. package/src/utils/format-lines.ts +26 -0
  108. package/src/utils/sorted-by.ts +3 -0
  109. package/src/utils/version.ts +10 -0
@@ -0,0 +1,300 @@
1
+ import type { Contract } from './contract';
2
+ import { ContractBuilder } from './contract';
3
+ import type { Access } from './set-access-control';
4
+ import { requireAccessControl, setAccessControl } from './set-access-control';
5
+ import { addPausable } from './add-pausable';
6
+ import { defineFunctions } from './utils/define-functions';
7
+ import type { CommonContractOptions } from './common-options';
8
+ import { withCommonContractDefaults, getSelfArg } from './common-options';
9
+ import { setInfo } from './set-info';
10
+ import { OptionsError } from './error';
11
+ import { contractDefaults as commonDefaults } from './common-options';
12
+ import { printContract } from './print';
13
+ import { toByteArray, toUint } from './utils/convert-strings';
14
+
15
+ export const defaults: Required<FungibleOptions> = {
16
+ name: 'MyToken',
17
+ symbol: 'MTK',
18
+ burnable: false,
19
+ pausable: false,
20
+ premint: '0',
21
+ mintable: false,
22
+ access: commonDefaults.access, // TODO: Determine whether Access Control options should be visible in the UI before they are implemented as modules
23
+ info: commonDefaults.info,
24
+ } as const;
25
+
26
+ export function printFungible(opts: FungibleOptions = defaults): string {
27
+ return printContract(buildFungible(opts));
28
+ }
29
+
30
+ export interface FungibleOptions extends CommonContractOptions {
31
+ name: string;
32
+ symbol: string;
33
+ burnable?: boolean;
34
+ pausable?: boolean;
35
+ premint?: string;
36
+ mintable?: boolean;
37
+ }
38
+
39
+ function withDefaults(opts: FungibleOptions): Required<FungibleOptions> {
40
+ return {
41
+ ...opts,
42
+ ...withCommonContractDefaults(opts),
43
+ burnable: opts.burnable ?? defaults.burnable,
44
+ pausable: opts.pausable ?? defaults.pausable,
45
+ premint: opts.premint || defaults.premint,
46
+ mintable: opts.mintable ?? defaults.mintable,
47
+ };
48
+ }
49
+
50
+ export function buildFungible(opts: FungibleOptions): Contract {
51
+ const c = new ContractBuilder(opts.name);
52
+
53
+ const allOpts = withDefaults(opts);
54
+
55
+ addBase(c, toByteArray(allOpts.name), toByteArray(allOpts.symbol), allOpts.pausable);
56
+
57
+ if (allOpts.premint) {
58
+ addPremint(c, allOpts.premint);
59
+ }
60
+
61
+ if (allOpts.pausable) {
62
+ addPausable(c, allOpts.access);
63
+ }
64
+
65
+ if (allOpts.burnable) {
66
+ addBurnable(c, allOpts.pausable);
67
+ }
68
+
69
+ if (allOpts.mintable) {
70
+ addMintable(c, allOpts.access, allOpts.pausable);
71
+ }
72
+
73
+ setAccessControl(c, allOpts.access);
74
+ setInfo(c, allOpts.info);
75
+
76
+ return c;
77
+ }
78
+
79
+ function addBase(c: ContractBuilder, name: string, symbol: string, pausable: boolean) {
80
+ // Set metadata
81
+ c.addConstructorCode(
82
+ `fungible::metadata::set_metadata(e, 18, String::from_str(e, "${name}"), String::from_str(e, "${symbol}"));`,
83
+ );
84
+
85
+ // Set token functions
86
+ c.addUseClause('openzeppelin_fungible_token', 'self', { alias: 'fungible' });
87
+ c.addUseClause('openzeppelin_fungible_token', 'FungibleToken');
88
+ c.addUseClause('soroban_sdk', 'contract');
89
+ c.addUseClause('soroban_sdk', 'contractimpl');
90
+ c.addUseClause('soroban_sdk', 'Address');
91
+ c.addUseClause('soroban_sdk', 'String');
92
+ c.addUseClause('soroban_sdk', 'Env');
93
+ c.addUseClause('soroban_sdk', 'Symbol');
94
+
95
+ const fungibleTokenTrait = {
96
+ name: 'FungibleToken',
97
+ for: c.name,
98
+ tags: ['contractimpl'],
99
+ };
100
+
101
+ c.addFunction(fungibleTokenTrait, functions.total_supply);
102
+ c.addFunction(fungibleTokenTrait, functions.balance);
103
+ c.addFunction(fungibleTokenTrait, functions.allowance);
104
+ c.addFunction(fungibleTokenTrait, functions.transfer);
105
+ c.addFunction(fungibleTokenTrait, functions.transfer_from);
106
+ c.addFunction(fungibleTokenTrait, functions.approve);
107
+ c.addFunction(fungibleTokenTrait, functions.decimals);
108
+ c.addFunction(fungibleTokenTrait, functions.name);
109
+ c.addFunction(fungibleTokenTrait, functions.symbol);
110
+
111
+ if (pausable) {
112
+ c.addUseClause('openzeppelin_pausable_macros', 'when_not_paused');
113
+ c.addFunctionTag(fungibleTokenTrait, functions.transfer, 'when_not_paused');
114
+ c.addFunctionTag(fungibleTokenTrait, functions.transfer_from, 'when_not_paused');
115
+ }
116
+ }
117
+
118
+ function addBurnable(c: ContractBuilder, pausable: boolean) {
119
+ c.addUseClause('openzeppelin_fungible_token', 'burnable::FungibleBurnable');
120
+ c.addUseClause('soroban_sdk', 'Address');
121
+
122
+ const fungibleBurnableTrait = {
123
+ name: 'FungibleBurnable',
124
+ for: c.name,
125
+ tags: ['contractimpl'],
126
+ section: 'Extensions',
127
+ };
128
+
129
+ c.addFunction(fungibleBurnableTrait, functions.burn);
130
+ c.addFunction(fungibleBurnableTrait, functions.burn_from);
131
+
132
+ if (pausable) {
133
+ c.addUseClause('openzeppelin_pausable_macros', 'when_not_paused');
134
+ c.addFunctionTag(fungibleBurnableTrait, functions.burn, 'when_not_paused');
135
+ c.addFunctionTag(fungibleBurnableTrait, functions.burn_from, 'when_not_paused');
136
+ }
137
+ }
138
+
139
+ export const premintPattern = /^(\d*\.?\d*)$/;
140
+
141
+ function addPremint(c: ContractBuilder, amount: string) {
142
+ if (amount !== undefined && amount !== '0') {
143
+ if (!premintPattern.test(amount)) {
144
+ throw new OptionsError({
145
+ premint: 'Not a valid number',
146
+ });
147
+ }
148
+
149
+ // TODO: handle signed int?
150
+ const premintAbsolute = toUint(getInitialSupply(amount, 18), 'premint', 'u128');
151
+
152
+ c.addUseClause('openzeppelin_fungible_token', 'mintable::FungibleMintable');
153
+ c.addUseClause('soroban_sdk', 'Address');
154
+
155
+ c.addConstructorArgument({ name: 'recipient', type: 'Address' });
156
+ c.addConstructorCode(`fungible::mintable::mint(e, &recipient, ${premintAbsolute});`);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Calculates the initial supply that would be used in an Fungible contract based on a given premint amount and number of decimals.
162
+ *
163
+ * @param premint Premint amount in token units, may be fractional
164
+ * @param decimals The number of decimals in the token
165
+ * @returns `premint` with zeros padded or removed based on `decimals`.
166
+ * @throws OptionsError if `premint` has more than one decimal character or is more precise than allowed by the `decimals` argument.
167
+ */
168
+ export function getInitialSupply(premint: string, decimals: number): string {
169
+ let result;
170
+ const premintSegments = premint.split('.');
171
+ if (premintSegments.length > 2) {
172
+ throw new OptionsError({
173
+ premint: 'Not a valid number',
174
+ });
175
+ } else {
176
+ const firstSegment = premintSegments[0] ?? '';
177
+ let lastSegment = premintSegments[1] ?? '';
178
+ if (decimals > lastSegment.length) {
179
+ try {
180
+ lastSegment += '0'.repeat(decimals - lastSegment.length);
181
+ } catch {
182
+ // .repeat gives an error if decimals number is too large
183
+ throw new OptionsError({
184
+ premint: 'Decimals number too large',
185
+ });
186
+ }
187
+ } else if (decimals < lastSegment.length) {
188
+ throw new OptionsError({
189
+ premint: 'Too many decimals',
190
+ });
191
+ }
192
+ // concat segments without leading zeros
193
+ result = firstSegment.concat(lastSegment).replace(/^0+/, '');
194
+ }
195
+ if (result.length === 0) {
196
+ result = '0';
197
+ }
198
+ return result;
199
+ }
200
+
201
+ function addMintable(c: ContractBuilder, access: Access, pausable: boolean) {
202
+ c.addUseClause('openzeppelin_fungible_token', 'mintable::FungibleMintable');
203
+
204
+ const fungibleMintableTrait = {
205
+ name: 'FungibleMintable',
206
+ for: c.name,
207
+ tags: ['contractimpl'],
208
+ section: 'Extensions',
209
+ };
210
+
211
+ c.addFunction(fungibleMintableTrait, functions.mint);
212
+
213
+ requireAccessControl(c, fungibleMintableTrait, functions.mint, access);
214
+
215
+ if (pausable) {
216
+ c.addFunctionTag(fungibleMintableTrait, functions.mint, 'when_not_paused');
217
+ }
218
+ }
219
+
220
+ const functions = defineFunctions({
221
+ // Token Functions
222
+ total_supply: {
223
+ args: [getSelfArg()],
224
+ returns: 'i128',
225
+ code: ['fungible::total_supply(e)'],
226
+ },
227
+ balance: {
228
+ args: [getSelfArg(), { name: 'account', type: 'Address' }],
229
+ returns: 'i128',
230
+ code: ['fungible::balance(e, &account)'],
231
+ },
232
+ allowance: {
233
+ args: [getSelfArg(), { name: 'owner', type: 'Address' }, { name: 'spender', type: 'Address' }],
234
+ returns: 'i128',
235
+ code: ['fungible::allowance(e, &owner, &spender)'],
236
+ },
237
+ transfer: {
238
+ args: [
239
+ getSelfArg(),
240
+ { name: 'from', type: 'Address' },
241
+ { name: 'to', type: 'Address' },
242
+ { name: 'amount', type: 'i128' },
243
+ ],
244
+ code: ['fungible::transfer(e, &from, &to, amount)'],
245
+ },
246
+ transfer_from: {
247
+ args: [
248
+ getSelfArg(),
249
+ { name: 'spender', type: 'Address' },
250
+ { name: 'from', type: 'Address' },
251
+ { name: 'to', type: 'Address' },
252
+ { name: 'amount', type: 'i128' },
253
+ ],
254
+ code: ['fungible::transfer_from(e, &spender, &from, &to, amount)'],
255
+ },
256
+ approve: {
257
+ args: [
258
+ getSelfArg(),
259
+ { name: 'owner', type: 'Address' },
260
+ { name: 'spender', type: 'Address' },
261
+ { name: 'amount', type: 'i128' },
262
+ { name: 'live_until_ledger', type: 'u32' },
263
+ ],
264
+ code: ['fungible::approve(e, &owner, &spender, amount, live_until_ledger)'],
265
+ },
266
+ decimals: {
267
+ args: [getSelfArg()],
268
+ returns: 'u32',
269
+ code: ['fungible::metadata::decimals(e)'],
270
+ },
271
+ name: {
272
+ args: [getSelfArg()],
273
+ returns: 'String',
274
+ code: ['fungible::metadata::name(e)'],
275
+ },
276
+ symbol: {
277
+ args: [getSelfArg()],
278
+ returns: 'String',
279
+ code: ['fungible::metadata::symbol(e)'],
280
+ },
281
+
282
+ // Extensions
283
+ burn: {
284
+ args: [getSelfArg(), { name: 'from', type: 'Address' }, { name: 'amount', type: 'i128' }],
285
+ code: ['fungible::burnable::burn(e, &from, amount)'],
286
+ },
287
+ burn_from: {
288
+ args: [
289
+ getSelfArg(),
290
+ { name: 'spender', type: 'Address' },
291
+ { name: 'from', type: 'Address' },
292
+ { name: 'amount', type: 'i128' },
293
+ ],
294
+ code: ['fungible::burnable::burn_from(e, &spender, &from, amount)'],
295
+ },
296
+ mint: {
297
+ args: [getSelfArg(), { name: 'account', type: 'Address' }, { name: 'amount', type: 'i128' }],
298
+ code: ['fungible::mintable::mint(e, &account, amount);'],
299
+ },
300
+ });
@@ -0,0 +1,32 @@
1
+ type Blueprint = Record<string, readonly unknown[]>;
2
+
3
+ type Alternatives<B extends Blueprint> = {
4
+ [k in keyof B]: B[k][number];
5
+ };
6
+
7
+ export function* generateAlternatives<B extends Blueprint>(blueprint: B): Generator<Alternatives<B>> {
8
+ const entries = Object.entries(blueprint).map(([key, values]) => ({
9
+ key,
10
+ values,
11
+ current: 0,
12
+ limit: values.length,
13
+ }));
14
+
15
+ for (; !done(); advance()) {
16
+ yield Object.fromEntries(entries.map(e => [e.key, e.values[e.current % e.limit]])) as Alternatives<B>;
17
+ }
18
+
19
+ function done() {
20
+ const last = entries[entries.length - 1];
21
+ return last?.current === last?.limit;
22
+ }
23
+
24
+ function advance() {
25
+ for (const e of entries) {
26
+ e.current = (e.current % e.limit) + 1;
27
+ if (e.current < e.limit) {
28
+ break;
29
+ }
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ import type { FungibleOptions } from '../fungible';
2
+ import { accessOptions } from '../set-access-control';
3
+ import { infoOptions } from '../set-info';
4
+ import { generateAlternatives } from './alternatives';
5
+
6
+ const booleans = [true, false];
7
+
8
+ const blueprint = {
9
+ name: ['MyToken'],
10
+ symbol: ['MTK'],
11
+ burnable: booleans,
12
+ pausable: booleans,
13
+ mintable: booleans,
14
+ premint: ['1'],
15
+ access: accessOptions,
16
+ info: infoOptions,
17
+ };
18
+
19
+ export function* generateFungibleOptions(): Generator<Required<FungibleOptions>> {
20
+ yield* generateAlternatives(blueprint);
21
+ }
@@ -0,0 +1,75 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+
5
+ import { generateFungibleOptions } from './fungible';
6
+ import type { GenericOptions, KindedOptions } from '../build-generic';
7
+ import { buildGeneric } from '../build-generic';
8
+ import { printContract } from '../print';
9
+ import { OptionsError } from '../error';
10
+ import type { Contract } from '../contract';
11
+
12
+ type Kind = keyof KindedOptions;
13
+
14
+ export function* generateOptions(kind?: Kind): Generator<GenericOptions> {
15
+ if (!kind || kind === 'Fungible') {
16
+ for (const kindOpts of generateFungibleOptions()) {
17
+ yield { kind: 'Fungible', ...kindOpts };
18
+ }
19
+ }
20
+ }
21
+
22
+ interface GeneratedContract {
23
+ id: string;
24
+ options: GenericOptions;
25
+ contract: Contract;
26
+ }
27
+
28
+ interface GeneratedSource extends GeneratedContract {
29
+ source: string;
30
+ }
31
+
32
+ function generateContractSubset(kind?: Kind): GeneratedContract[] {
33
+ const contracts = [];
34
+
35
+ for (const options of generateOptions(kind)) {
36
+ const id = crypto.createHash('sha1').update(JSON.stringify(options)).digest().toString('hex');
37
+
38
+ try {
39
+ const contract = buildGeneric(options);
40
+ contracts.push({ id, options, contract });
41
+ } catch (e: unknown) {
42
+ if (e instanceof OptionsError) {
43
+ continue;
44
+ } else {
45
+ throw e;
46
+ }
47
+ }
48
+ }
49
+
50
+ return contracts;
51
+ }
52
+
53
+ export function* generateSources(uniqueName?: boolean, kind?: Kind): Generator<GeneratedSource> {
54
+ let counter = 1;
55
+ for (const c of generateContractSubset(kind)) {
56
+ if (uniqueName) {
57
+ c.contract.name = `Contract${counter++}`;
58
+ }
59
+ const source = printContract(c.contract);
60
+ yield { ...c, source };
61
+ }
62
+ }
63
+
64
+ export async function writeGeneratedSources(dir: string, uniqueName?: boolean, kind?: Kind): Promise<string[]> {
65
+ await fs.mkdir(dir, { recursive: true });
66
+ const contractNames = [];
67
+
68
+ for (const { id, contract, source } of generateSources(uniqueName, kind)) {
69
+ const name = uniqueName ? contract.name : id;
70
+ await fs.writeFile(path.format({ dir, name, ext: '.rs' }), source);
71
+ contractNames.push(name);
72
+ }
73
+
74
+ return contractNames;
75
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ export type { GenericOptions, KindedOptions } from './build-generic';
2
+ export { buildGeneric } from './build-generic';
3
+
4
+ export type { Contract } from './contract';
5
+ export { ContractBuilder } from './contract';
6
+
7
+ export { printContract } from './print';
8
+
9
+ export type { Access } from './set-access-control';
10
+ export type { Info } from './set-info';
11
+
12
+ export { premintPattern } from './fungible';
13
+
14
+ export { defaults as infoDefaults } from './set-info';
15
+
16
+ export type { OptionsErrorMessages } from './error';
17
+ export { OptionsError } from './error';
18
+
19
+ export type { Kind } from './kind';
20
+ export { sanitizeKind } from './kind';
21
+
22
+ export { contractsVersion, contractsVersionTag, compatibleContractsSemver } from './utils/version';
23
+
24
+ export { fungible } from './api';
package/src/kind.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { GenericOptions } from './build-generic';
2
+
3
+ export type Kind = GenericOptions['kind'];
4
+
5
+ export function sanitizeKind(kind: unknown): Kind {
6
+ if (typeof kind === 'string') {
7
+ const sanitized = kind.replace(/^(ERC|.)/i, c => c.toUpperCase());
8
+ if (isKind(sanitized)) {
9
+ return sanitized;
10
+ }
11
+ }
12
+ return 'Fungible';
13
+ }
14
+
15
+ function isKind<T>(value: Kind | T): value is Kind {
16
+ switch (value) {
17
+ case 'Fungible':
18
+ return true;
19
+
20
+ default: {
21
+ // Static assert that we've checked all kinds.
22
+ const _: T = value;
23
+ return false;
24
+ }
25
+ }
26
+ }