@openzeppelin/wizard-stellar 0.6.1 → 0.6.3

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 (50) hide show
  1. package/dist/api.d.ts +3 -0
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +7 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/build-generic.d.ts +4 -0
  6. package/dist/build-generic.d.ts.map +1 -1
  7. package/dist/build-generic.js +3 -0
  8. package/dist/build-generic.js.map +1 -1
  9. package/dist/generate/sources.d.ts.map +1 -1
  10. package/dist/generate/sources.js +6 -0
  11. package/dist/generate/sources.js.map +1 -1
  12. package/dist/generate/vault.d.ts +3 -0
  13. package/dist/generate/vault.d.ts.map +1 -0
  14. package/dist/generate/vault.js +21 -0
  15. package/dist/generate/vault.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/kind.js +1 -0
  21. package/dist/kind.js.map +1 -1
  22. package/dist/set-info.d.ts.map +1 -1
  23. package/dist/set-info.js +17 -2
  24. package/dist/set-info.js.map +1 -1
  25. package/dist/utils/compile-test.d.ts.map +1 -1
  26. package/dist/utils/compile-test.js +4 -1
  27. package/dist/utils/compile-test.js.map +1 -1
  28. package/dist/vault.d.ts +16 -0
  29. package/dist/vault.d.ts.map +1 -0
  30. package/dist/vault.js +334 -0
  31. package/dist/vault.js.map +1 -0
  32. package/dist/zip-rust.d.ts.map +1 -1
  33. package/dist/zip-rust.js +1 -1
  34. package/dist/zip-rust.js.map +1 -1
  35. package/dist/zip-shared.d.ts +1 -0
  36. package/dist/zip-shared.d.ts.map +1 -1
  37. package/dist/zip-shared.js +60 -1
  38. package/dist/zip-shared.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/api.ts +13 -0
  41. package/src/build-generic.ts +6 -0
  42. package/src/generate/sources.ts +7 -0
  43. package/src/generate/vault.ts +21 -0
  44. package/src/index.ts +2 -0
  45. package/src/kind.ts +1 -0
  46. package/src/set-info.ts +21 -2
  47. package/src/utils/compile-test.ts +5 -1
  48. package/src/vault.ts +378 -0
  49. package/src/zip-rust.ts +5 -1
  50. package/src/zip-shared.ts +62 -0
@@ -45,7 +45,11 @@ async function rewriteWorkspaceCargoForLocalContracts(workspaceCargoPath: string
45
45
  readFile(upstreamCargoPath, 'utf8'),
46
46
  ]);
47
47
 
48
- const sorobanMatch = upstreamCargo.match(/^\s*soroban-sdk\s*=\s*"([^"]+)"/m);
48
+ // Match both the bare-string form (`soroban-sdk = "26.1.0"`) and the table
49
+ // form (`soroban-sdk = { version = "26.1.0", features = [...] }`).
50
+ const sorobanMatch =
51
+ upstreamCargo.match(/^\s*soroban-sdk\s*=\s*"([^"]+)"/m) ??
52
+ upstreamCargo.match(/^\s*soroban-sdk\s*=\s*\{[^}]*?version\s*=\s*"([^"]+)"/m);
49
53
  if (sorobanMatch === null || sorobanMatch[1] === undefined) {
50
54
  throw new Error(`Unable to find soroban-sdk version in ${upstreamCargoPath}`);
51
55
  }
package/src/vault.ts ADDED
@@ -0,0 +1,378 @@
1
+ import { ContractBuilder } from './contract';
2
+ import type { Access } from './set-access-control';
3
+ import { setAccessControl } from './set-access-control';
4
+ import { addPausable } from './add-pausable';
5
+ import { addUpgradeable } from './add-upgradeable';
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 { contractDefaults as commonDefaults } from './common-options';
11
+ import { printContract } from './print';
12
+ import { toByteArray, toUint } from './utils/convert-strings';
13
+ import { OptionsError } from './error';
14
+ import { pickKeys } from '@openzeppelin/wizard-common';
15
+
16
+ // Upper bound enforced by the library's `Vault::set_decimals_offset`
17
+ // (`stellar_tokens::vault::MAX_DECIMALS_OFFSET`); a larger value panics on deploy.
18
+ const MAX_DECIMALS_OFFSET = 10;
19
+
20
+ export const defaults: Required<VaultOptions> = {
21
+ name: 'MyVault',
22
+ symbol: 'MTK',
23
+ decimalsOffset: '0',
24
+ pausable: false,
25
+ upgradeable: false,
26
+ access: commonDefaults.access,
27
+ info: commonDefaults.info,
28
+ explicitImplementations: commonDefaults.explicitImplementations,
29
+ } as const;
30
+
31
+ export function printVault(opts: VaultOptions = defaults): string {
32
+ return printContract(buildVault(opts));
33
+ }
34
+
35
+ export interface VaultOptions extends CommonContractOptions {
36
+ name: string;
37
+ symbol: string;
38
+ decimalsOffset?: string;
39
+ pausable?: boolean;
40
+ upgradeable?: boolean;
41
+ }
42
+
43
+ export function withDefaults(opts: VaultOptions): Required<VaultOptions> {
44
+ return {
45
+ ...opts,
46
+ ...withCommonContractDefaults(opts),
47
+ decimalsOffset: opts.decimalsOffset ?? defaults.decimalsOffset,
48
+ pausable: opts.pausable ?? defaults.pausable,
49
+ upgradeable: opts.upgradeable ?? defaults.upgradeable,
50
+ };
51
+ }
52
+
53
+ export function isAccessControlRequired(opts: Partial<VaultOptions>): boolean {
54
+ return opts.pausable === true || opts.upgradeable === true;
55
+ }
56
+
57
+ export function buildVault(opts: VaultOptions): ContractBuilder {
58
+ const c = new ContractBuilder(opts.name);
59
+
60
+ const allOpts = withDefaults(opts);
61
+
62
+ const decimalsOffset = toUint(allOpts.decimalsOffset, 'decimalsOffset', 'u32');
63
+ if (decimalsOffset > MAX_DECIMALS_OFFSET) {
64
+ throw new OptionsError({
65
+ decimalsOffset: `Maximum decimals offset is ${MAX_DECIMALS_OFFSET}`,
66
+ });
67
+ }
68
+
69
+ addBase(
70
+ c,
71
+ toByteArray(allOpts.name),
72
+ toByteArray(allOpts.symbol),
73
+ decimalsOffset,
74
+ allOpts.pausable,
75
+ allOpts.explicitImplementations,
76
+ );
77
+
78
+ if (allOpts.pausable) {
79
+ addPausable(c, allOpts.access, allOpts.explicitImplementations);
80
+ }
81
+
82
+ if (allOpts.upgradeable) {
83
+ addUpgradeable(c, allOpts.access, allOpts.explicitImplementations);
84
+ }
85
+
86
+ setAccessControl(c, allOpts.access, allOpts.explicitImplementations);
87
+
88
+ // The AccessControl trait surface (e.g. `get_existing_roles`, role lookups)
89
+ // references `Symbol` and `Vec`, which the contracttrait macro and explicit
90
+ // implementations need in scope even when no function is role-gated.
91
+ if (allOpts.access === 'roles') {
92
+ c.addUseClause('soroban_sdk', 'Symbol');
93
+ c.addUseClause('soroban_sdk', 'Vec');
94
+ }
95
+
96
+ setInfo(c, allOpts.info);
97
+
98
+ return c;
99
+ }
100
+
101
+ function addBase(
102
+ c: ContractBuilder,
103
+ name: string,
104
+ symbol: string,
105
+ decimalsOffset: bigint,
106
+ pausable: boolean,
107
+ explicitImplementations: boolean,
108
+ ) {
109
+ // The underlying asset is supplied at deployment, while the virtual decimals
110
+ // offset is a baked-in configuration value. The offset must be set before the
111
+ // metadata because the vault derives its decimals from the underlying asset's
112
+ // decimals plus the offset.
113
+ c.addConstructorArgument({ name: 'asset', type: 'Address' });
114
+ c.addConstructorCode('Vault::set_asset(e, asset);');
115
+ c.addConstructorCode(`Vault::set_decimals_offset(e, ${decimalsOffset});`);
116
+ c.addConstructorCode(
117
+ `Base::set_metadata(e, Self::decimals(e), String::from_str(e, "${name}"), String::from_str(e, "${symbol}"));`,
118
+ );
119
+
120
+ // Set token functions
121
+ c.addUseClause('stellar_tokens::fungible', 'Base');
122
+ c.addUseClause('stellar_tokens::fungible', 'FungibleToken');
123
+ c.addUseClause('stellar_tokens::vault', 'FungibleVault');
124
+ c.addUseClause('stellar_tokens::vault', 'Vault');
125
+ // The base token functions we emit delegate to `Self::ContractType::...`,
126
+ // which resolve through the `ContractOverrides` trait that `Vault` implements,
127
+ // so it must be in scope. This happens with explicit implementations (all
128
+ // functions are spelled out) and when pausable adds transfer overrides.
129
+ if (explicitImplementations || pausable) c.addUseClause('stellar_tokens::fungible', 'ContractOverrides');
130
+ c.addUseClause('soroban_sdk', 'contract');
131
+ c.addUseClause('soroban_sdk', 'contractimpl');
132
+ c.addUseClause('soroban_sdk', 'String');
133
+ c.addUseClause('soroban_sdk', 'Env');
134
+ c.addUseClause('soroban_sdk', 'Address');
135
+ c.addUseClause('soroban_sdk', 'MuxedAddress');
136
+
137
+ const fungibleTokenTrait = {
138
+ traitName: 'FungibleToken',
139
+ structName: c.name,
140
+ tags: explicitImplementations ? ['contractimpl'] : ['contractimpl(contracttrait)'],
141
+ assocType: 'type ContractType = Vault;',
142
+ };
143
+
144
+ c.addTraitImplBlock(fungibleTokenTrait);
145
+
146
+ if (explicitImplementations) {
147
+ c.addTraitForEachFunctions(fungibleTokenTrait, fungibleTokenTraitFunctions);
148
+ } else {
149
+ // The vault overrides decimals to derive them from the underlying asset and
150
+ // the virtual decimals offset.
151
+ c.addTraitFunction(fungibleTokenTrait, functions.decimals);
152
+ }
153
+
154
+ if (pausable) {
155
+ c.addUseClause('stellar_macros', 'when_not_paused');
156
+
157
+ c.addTraitFunction(fungibleTokenTrait, functions.transfer);
158
+ c.addFunctionTag(functions.transfer, 'when_not_paused', fungibleTokenTrait);
159
+
160
+ c.addTraitFunction(fungibleTokenTrait, functions.transfer_from);
161
+ c.addFunctionTag(functions.transfer_from, 'when_not_paused', fungibleTokenTrait);
162
+ }
163
+
164
+ const fungibleVaultTrait = {
165
+ traitName: 'FungibleVault',
166
+ structName: c.name,
167
+ tags: explicitImplementations ? ['contractimpl'] : ['contractimpl(contracttrait)'],
168
+ section: 'Extensions',
169
+ };
170
+
171
+ if (explicitImplementations) {
172
+ c.addTraitForEachFunctions(fungibleVaultTrait, fungibleVaultTraitFunctions);
173
+ } else {
174
+ c.addTraitImplBlock(fungibleVaultTrait);
175
+ }
176
+ }
177
+
178
+ export const functions = defineFunctions({
179
+ // FungibleToken Trait
180
+ total_supply: {
181
+ args: [getSelfArg()],
182
+ returns: 'i128',
183
+ code: ['Self::ContractType::total_supply(e)'],
184
+ },
185
+ balance: {
186
+ args: [getSelfArg(), { name: 'account', type: 'Address' }],
187
+ returns: 'i128',
188
+ code: ['Self::ContractType::balance(e, &account)'],
189
+ },
190
+ allowance: {
191
+ args: [getSelfArg(), { name: 'owner', type: 'Address' }, { name: 'spender', type: 'Address' }],
192
+ returns: 'i128',
193
+ code: ['Self::ContractType::allowance(e, &owner, &spender)'],
194
+ },
195
+ transfer: {
196
+ args: [
197
+ getSelfArg(),
198
+ { name: 'from', type: 'Address' },
199
+ { name: 'to', type: 'MuxedAddress' },
200
+ { name: 'amount', type: 'i128' },
201
+ ],
202
+ code: ['Self::ContractType::transfer(e, &from, &to, amount)'],
203
+ },
204
+ transfer_from: {
205
+ args: [
206
+ getSelfArg(),
207
+ { name: 'spender', type: 'Address' },
208
+ { name: 'from', type: 'Address' },
209
+ { name: 'to', type: 'Address' },
210
+ { name: 'amount', type: 'i128' },
211
+ ],
212
+ code: ['Self::ContractType::transfer_from(e, &spender, &from, &to, amount)'],
213
+ },
214
+ approve: {
215
+ args: [
216
+ getSelfArg(),
217
+ { name: 'owner', type: 'Address' },
218
+ { name: 'spender', type: 'Address' },
219
+ { name: 'amount', type: 'i128' },
220
+ { name: 'live_until_ledger', type: 'u32' },
221
+ ],
222
+ code: ['Self::ContractType::approve(e, &owner, &spender, amount, live_until_ledger)'],
223
+ },
224
+ // The vault derives decimals from the underlying asset's decimals plus the
225
+ // virtual offset, so it overrides the default metadata-based implementation.
226
+ decimals: {
227
+ args: [getSelfArg()],
228
+ returns: 'u32',
229
+ code: ['Vault::decimals(e)'],
230
+ },
231
+ name: {
232
+ args: [getSelfArg()],
233
+ returns: 'String',
234
+ code: ['Self::ContractType::name(e)'],
235
+ },
236
+ symbol: {
237
+ args: [getSelfArg()],
238
+ returns: 'String',
239
+ code: ['Self::ContractType::symbol(e)'],
240
+ },
241
+
242
+ // FungibleVault Trait
243
+ query_asset: {
244
+ args: [getSelfArg()],
245
+ returns: 'Address',
246
+ code: ['Self::ContractType::query_asset(e)'],
247
+ },
248
+ total_assets: {
249
+ args: [getSelfArg()],
250
+ returns: 'i128',
251
+ code: ['Self::ContractType::total_assets(e)'],
252
+ },
253
+ convert_to_shares: {
254
+ args: [getSelfArg(), { name: 'assets', type: 'i128' }],
255
+ returns: 'i128',
256
+ code: ['Self::ContractType::convert_to_shares(e, assets)'],
257
+ },
258
+ convert_to_assets: {
259
+ args: [getSelfArg(), { name: 'shares', type: 'i128' }],
260
+ returns: 'i128',
261
+ code: ['Self::ContractType::convert_to_assets(e, shares)'],
262
+ },
263
+ max_deposit: {
264
+ args: [getSelfArg(), { name: 'receiver', type: 'Address' }],
265
+ returns: 'i128',
266
+ code: ['Self::ContractType::max_deposit(e, receiver)'],
267
+ },
268
+ preview_deposit: {
269
+ args: [getSelfArg(), { name: 'assets', type: 'i128' }],
270
+ returns: 'i128',
271
+ code: ['Self::ContractType::preview_deposit(e, assets)'],
272
+ },
273
+ deposit: {
274
+ args: [
275
+ getSelfArg(),
276
+ { name: 'assets', type: 'i128' },
277
+ { name: 'receiver', type: 'Address' },
278
+ { name: 'from', type: 'Address' },
279
+ { name: 'operator', type: 'Address' },
280
+ ],
281
+ returns: 'i128',
282
+ code: ['Self::ContractType::deposit(e, assets, receiver, from, operator)'],
283
+ },
284
+ max_mint: {
285
+ args: [getSelfArg(), { name: 'receiver', type: 'Address' }],
286
+ returns: 'i128',
287
+ code: ['Self::ContractType::max_mint(e, receiver)'],
288
+ },
289
+ preview_mint: {
290
+ args: [getSelfArg(), { name: 'shares', type: 'i128' }],
291
+ returns: 'i128',
292
+ code: ['Self::ContractType::preview_mint(e, shares)'],
293
+ },
294
+ mint: {
295
+ args: [
296
+ getSelfArg(),
297
+ { name: 'shares', type: 'i128' },
298
+ { name: 'receiver', type: 'Address' },
299
+ { name: 'from', type: 'Address' },
300
+ { name: 'operator', type: 'Address' },
301
+ ],
302
+ returns: 'i128',
303
+ code: ['Self::ContractType::mint(e, shares, receiver, from, operator)'],
304
+ },
305
+ max_withdraw: {
306
+ args: [getSelfArg(), { name: 'owner', type: 'Address' }],
307
+ returns: 'i128',
308
+ code: ['Self::ContractType::max_withdraw(e, owner)'],
309
+ },
310
+ preview_withdraw: {
311
+ args: [getSelfArg(), { name: 'assets', type: 'i128' }],
312
+ returns: 'i128',
313
+ code: ['Self::ContractType::preview_withdraw(e, assets)'],
314
+ },
315
+ withdraw: {
316
+ args: [
317
+ getSelfArg(),
318
+ { name: 'assets', type: 'i128' },
319
+ { name: 'receiver', type: 'Address' },
320
+ { name: 'owner', type: 'Address' },
321
+ { name: 'operator', type: 'Address' },
322
+ ],
323
+ returns: 'i128',
324
+ code: ['Self::ContractType::withdraw(e, assets, receiver, owner, operator)'],
325
+ },
326
+ max_redeem: {
327
+ args: [getSelfArg(), { name: 'owner', type: 'Address' }],
328
+ returns: 'i128',
329
+ code: ['Self::ContractType::max_redeem(e, owner)'],
330
+ },
331
+ preview_redeem: {
332
+ args: [getSelfArg(), { name: 'shares', type: 'i128' }],
333
+ returns: 'i128',
334
+ code: ['Self::ContractType::preview_redeem(e, shares)'],
335
+ },
336
+ redeem: {
337
+ args: [
338
+ getSelfArg(),
339
+ { name: 'shares', type: 'i128' },
340
+ { name: 'receiver', type: 'Address' },
341
+ { name: 'owner', type: 'Address' },
342
+ { name: 'operator', type: 'Address' },
343
+ ],
344
+ returns: 'i128',
345
+ code: ['Self::ContractType::redeem(e, shares, receiver, owner, operator)'],
346
+ },
347
+ });
348
+
349
+ const fungibleTokenTraitFunctions = pickKeys(functions, [
350
+ 'total_supply',
351
+ 'balance',
352
+ 'allowance',
353
+ 'transfer',
354
+ 'transfer_from',
355
+ 'approve',
356
+ 'decimals',
357
+ 'name',
358
+ 'symbol',
359
+ ]);
360
+
361
+ const fungibleVaultTraitFunctions = pickKeys(functions, [
362
+ 'query_asset',
363
+ 'total_assets',
364
+ 'convert_to_shares',
365
+ 'convert_to_assets',
366
+ 'max_deposit',
367
+ 'preview_deposit',
368
+ 'deposit',
369
+ 'max_mint',
370
+ 'preview_mint',
371
+ 'mint',
372
+ 'max_withdraw',
373
+ 'preview_withdraw',
374
+ 'withdraw',
375
+ 'max_redeem',
376
+ 'preview_redeem',
377
+ 'redeem',
378
+ ]);
package/src/zip-rust.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  getRequiredLibDependencies,
9
9
  printContractCargo,
10
10
  printRustNameTest,
11
+ printVaultRustTest,
11
12
  workspaceCargo,
12
13
  } from './zip-shared';
13
14
 
@@ -37,7 +38,10 @@ export const createRustZipEnvironment = (c: Contract, opts: GenericOptions) => {
37
38
  const requiredLibDeps = getRequiredLibDependencies(c);
38
39
 
39
40
  zip.file(`contracts/${contractName}/src/contract.rs`, removeCreateLevelAttributes(printContract(c)));
40
- zip.file(`contracts/${contractName}/src/test.rs`, printRustNameTest(c));
41
+ zip.file(
42
+ `contracts/${contractName}/src/test.rs`,
43
+ opts?.kind === 'Vault' ? printVaultRustTest(c) : printRustNameTest(c),
44
+ );
41
45
  zip.file(`contracts/${contractName}/src/lib.rs`, createRustLibFile);
42
46
  zip.file(`contracts/${contractName}/Cargo.toml`, printContractCargo(contractName, requiredLibDeps));
43
47
  zip.file('Cargo.toml', workspaceCargo(requiredLibDeps));
package/src/zip-shared.ts CHANGED
@@ -39,6 +39,68 @@ fn initial_state() {
39
39
  // Add more tests bellow
40
40
  `;
41
41
 
42
+ // The vault's constructor takes an underlying asset address and derives its own
43
+ // decimals by calling into that asset contract. A generated address would have
44
+ // no `decimals()` to call, so the test deploys a minimal mock fungible asset
45
+ // first and registers the vault against it. A single-element tuple needs a
46
+ // trailing comma in Rust.
47
+ export const printVaultRustTest = (c: Pick<Contract, 'constructorArgs' | 'name'>) => {
48
+ const registerArgs = (c.constructorArgs || []).map(arg =>
49
+ arg.name === 'asset' ? 'asset_address.clone()' : 'Address::generate(&env)',
50
+ );
51
+ const registerTuple = `(${registerArgs.join(', ')}${registerArgs.length === 1 ? ',' : ''})`;
52
+
53
+ return `#![cfg(test)]
54
+
55
+ extern crate std;
56
+
57
+ use soroban_sdk::{
58
+ contract, contractimpl, testutils::Address as _, Address, Env, MuxedAddress, String,
59
+ };
60
+ use stellar_tokens::fungible::{Base, FungibleToken};
61
+
62
+ use crate::contract::{ ${c.name}, ${c.name}Client };
63
+
64
+ // Mock asset contract: a minimal fungible token used as the vault's underlying asset.
65
+ #[contract]
66
+ pub struct MockAssetContract;
67
+
68
+ #[contractimpl]
69
+ impl MockAssetContract {
70
+ pub fn __constructor(e: &Env, initial_supply: i128, admin: Address) {
71
+ Base::set_metadata(
72
+ e,
73
+ 18,
74
+ String::from_str(e, "Mock Asset Token"),
75
+ String::from_str(e, "MAT"),
76
+ );
77
+ Base::mint(e, &admin, initial_supply);
78
+ }
79
+ }
80
+
81
+ #[contractimpl(contracttrait)]
82
+ impl FungibleToken for MockAssetContract {
83
+ type ContractType = Base;
84
+ }
85
+
86
+ #[test]
87
+ fn initial_state() {
88
+ let env = Env::default();
89
+
90
+ let admin = Address::generate(&env);
91
+ let asset_address = env.register(MockAssetContract, (1_000_000_000i128, admin));
92
+
93
+ let contract_addr = env.register(${c.name}, ${registerTuple});
94
+ let client = ${c.name}Client::new(&env, &contract_addr);
95
+
96
+ assert_eq!(client.query_asset(), asset_address);
97
+ assert_eq!(client.name(), String::from_str(&env, "${c.name}"));
98
+ }
99
+
100
+ // Add more tests bellow
101
+ `;
102
+ };
103
+
42
104
  const libDependencies = [
43
105
  'stellar-tokens',
44
106
  'stellar-access',