@openzeppelin/wizard-stellar 0.6.3 → 0.6.5
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/README.md +4 -0
- package/dist/account.d.ts +19 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +251 -0
- package/dist/account.js.map +1 -0
- package/dist/add-upgradeable.d.ts +9 -0
- package/dist/add-upgradeable.d.ts.map +1 -1
- package/dist/add-upgradeable.js +25 -0
- package/dist/add-upgradeable.js.map +1 -1
- package/dist/api.d.ts +7 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -1
- package/dist/api.js.map +1 -1
- package/dist/build-generic.d.ts +4 -0
- package/dist/build-generic.d.ts.map +1 -1
- package/dist/build-generic.js +3 -0
- package/dist/build-generic.js.map +1 -1
- package/dist/contract.d.ts +21 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +11 -0
- package/dist/contract.js.map +1 -1
- package/dist/fungible.d.ts.map +1 -1
- package/dist/fungible.js +1 -1
- package/dist/fungible.js.map +1 -1
- package/dist/generate/account.d.ts +3 -0
- package/dist/generate/account.d.ts.map +1 -0
- package/dist/generate/account.js +21 -0
- package/dist/generate/account.js.map +1 -0
- package/dist/generate/sources.d.ts.map +1 -1
- package/dist/generate/sources.js +6 -0
- package/dist/generate/sources.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/kind.js +1 -0
- package/dist/kind.js.map +1 -1
- package/dist/print.js +39 -9
- package/dist/print.js.map +1 -1
- package/dist/utils/compile-test.d.ts.map +1 -1
- package/dist/utils/compile-test.js +1 -0
- package/dist/utils/compile-test.js.map +1 -1
- package/dist/zip-rust.d.ts.map +1 -1
- package/dist/zip-rust.js +14 -1
- package/dist/zip-rust.js.map +1 -1
- package/dist/zip-shared.d.ts +14 -0
- package/dist/zip-shared.d.ts.map +1 -1
- package/dist/zip-shared.js +212 -1
- package/dist/zip-shared.js.map +1 -1
- package/package.json +1 -1
- package/src/account.ts +319 -0
- package/src/add-upgradeable.ts +29 -0
- package/src/api.ts +12 -0
- package/src/build-generic.ts +6 -0
- package/src/contract.ts +24 -2
- package/src/fungible.ts +1 -1
- package/src/generate/account.ts +21 -0
- package/src/generate/sources.ts +7 -0
- package/src/index.ts +2 -0
- package/src/kind.ts +1 -0
- package/src/print.ts +47 -18
- package/src/utils/compile-test.ts +1 -0
- package/src/zip-rust.ts +16 -4
- package/src/zip-shared.ts +231 -0
package/src/contract.ts
CHANGED
|
@@ -35,7 +35,11 @@ export interface BaseTraitImplBlock {
|
|
|
35
35
|
traitName: string;
|
|
36
36
|
structName: string;
|
|
37
37
|
tags: string[];
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Associated type bindings for the impl block. An array declares several of
|
|
40
|
+
* them, one per line (e.g. `CustomAccountInterface`'s `Error` and `Signature`).
|
|
41
|
+
*/
|
|
42
|
+
assocType?: string | string[];
|
|
39
43
|
section?: string;
|
|
40
44
|
/**
|
|
41
45
|
* Priority for which trait to print first.
|
|
@@ -70,6 +74,12 @@ export interface Variable {
|
|
|
70
74
|
export interface Argument {
|
|
71
75
|
name: string;
|
|
72
76
|
type?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Comment lines printed above the argument. Forces one argument per line, so
|
|
79
|
+
* only worth using where the caller needs guidance that the type cannot carry,
|
|
80
|
+
* such as where to obtain an address.
|
|
81
|
+
*/
|
|
82
|
+
comment?: string[];
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
export class ContractBuilder implements Contract {
|
|
@@ -215,7 +225,7 @@ export class ContractBuilder implements Contract {
|
|
|
215
225
|
return contractFn;
|
|
216
226
|
}
|
|
217
227
|
|
|
218
|
-
overrideAssocType(traitName: string, newAssocType: string): void {
|
|
228
|
+
overrideAssocType(traitName: string, newAssocType: string | string[]): void {
|
|
219
229
|
const trait = this.implementedTraitsMap.get(traitName);
|
|
220
230
|
if (trait) {
|
|
221
231
|
trait.assocType = newAssocType;
|
|
@@ -274,6 +284,18 @@ export class ContractBuilder implements Contract {
|
|
|
274
284
|
this.constructorCode.push(code);
|
|
275
285
|
}
|
|
276
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Appends a block of constructor code verbatim, keeping duplicate lines.
|
|
289
|
+
*
|
|
290
|
+
* `addConstructorCode` deduplicates, which is right for the idempotent setup
|
|
291
|
+
* statements each generator contributes, but wrong for a multi-line block
|
|
292
|
+
* where lines such as `}` legitimately repeat. Empty entries separate groups
|
|
293
|
+
* and are printed as blank lines.
|
|
294
|
+
*/
|
|
295
|
+
addConstructorCodeBlock(lines: string[]): void {
|
|
296
|
+
this.constructorCode.push(...lines);
|
|
297
|
+
}
|
|
298
|
+
|
|
277
299
|
addDerives(derive: string): void {
|
|
278
300
|
this.derivesSet.add(derive);
|
|
279
301
|
}
|
package/src/fungible.ts
CHANGED
|
@@ -250,7 +250,7 @@ function addBurnable(c: ContractBuilder, votes: boolean, pausable: boolean, expl
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
export const premintPattern =
|
|
253
|
+
export const premintPattern = /^\d*(?:\.\d*)?$/;
|
|
254
254
|
|
|
255
255
|
function addPremint(c: ContractBuilder, amount: string, decimals: bigint, votes: boolean) {
|
|
256
256
|
if (amount !== undefined && amount !== '0') {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AccountOptions } from '../account';
|
|
2
|
+
import { policyOptions } from '../account';
|
|
3
|
+
import { infoOptions } from '../set-info';
|
|
4
|
+
import { generateAlternatives } from './alternatives';
|
|
5
|
+
|
|
6
|
+
const booleans = [true, false];
|
|
7
|
+
|
|
8
|
+
const blueprint = {
|
|
9
|
+
name: ['MyAccount'],
|
|
10
|
+
delegatedSigners: booleans,
|
|
11
|
+
ed25519Signers: booleans,
|
|
12
|
+
webauthnSigners: booleans,
|
|
13
|
+
policy: policyOptions,
|
|
14
|
+
executionEntryPoint: booleans,
|
|
15
|
+
upgradeable: booleans,
|
|
16
|
+
info: infoOptions,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function* generateAccountOptions(): Generator<Required<AccountOptions>> {
|
|
20
|
+
yield* generateAlternatives(blueprint);
|
|
21
|
+
}
|
package/src/generate/sources.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
|
|
5
|
+
import { generateAccountOptions } from './account';
|
|
5
6
|
import { generateFungibleOptions } from './fungible';
|
|
6
7
|
import { generateGovernorOptions } from './governor';
|
|
7
8
|
import { generateNonFungibleOptions } from './non-fungible';
|
|
@@ -16,6 +17,12 @@ import type { Contract } from '../contract';
|
|
|
16
17
|
type Kind = keyof KindedOptions;
|
|
17
18
|
|
|
18
19
|
export function* generateOptions(kind?: Kind): Generator<GenericOptions> {
|
|
20
|
+
if (!kind || kind === 'Account') {
|
|
21
|
+
for (const kindOpts of generateAccountOptions()) {
|
|
22
|
+
yield { kind: 'Account', ...kindOpts };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
if (!kind || kind === 'Fungible') {
|
|
20
27
|
for (const kindOpts of generateFungibleOptions()) {
|
|
21
28
|
yield { kind: 'Fungible', ...kindOpts };
|
package/src/index.ts
CHANGED
|
@@ -21,12 +21,14 @@ export { sanitizeKind } from './kind';
|
|
|
21
21
|
|
|
22
22
|
export { contractsVersion, contractsVersionTag, compatibleContractsSemver } from './utils/version';
|
|
23
23
|
|
|
24
|
+
export { account } from './api';
|
|
24
25
|
export { fungible } from './api';
|
|
25
26
|
export { governor } from './api';
|
|
26
27
|
export { nonFungible } from './api';
|
|
27
28
|
export { stablecoin } from './api';
|
|
28
29
|
export { vault } from './api';
|
|
29
30
|
|
|
31
|
+
export type { AccountOptions, Policy } from './account';
|
|
30
32
|
export type { FungibleOptions } from './fungible';
|
|
31
33
|
export type { GovernorOptions } from './governor';
|
|
32
34
|
export type { NonFungibleOptions } from './non-fungible';
|
package/src/kind.ts
CHANGED
package/src/print.ts
CHANGED
|
@@ -64,8 +64,17 @@ function printContractErrors(contract: Contract): Lines[] {
|
|
|
64
64
|
];
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Lines that open, close or continue a block or expression are not statements,
|
|
68
|
+
// so they must not be terminated. Needed by multi-line code such as a `for` loop
|
|
69
|
+
// or a call whose arguments are spread over several lines.
|
|
70
|
+
const CONTINUATION_LINE_ENDINGS = ['{', '}', ';', ',', '('];
|
|
71
|
+
|
|
72
|
+
function needsSemicolon(line: string): boolean {
|
|
73
|
+
return line.length > 0 && !CONTINUATION_LINE_ENDINGS.includes(line.at(-1)!);
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
function withSemicolons(lines: string[]): string[] {
|
|
68
|
-
return lines.map(line => (line
|
|
77
|
+
return lines.map(line => (needsSemicolon(line) ? line + ';' : line));
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
function printVariables(contract: Contract): string[] {
|
|
@@ -208,8 +217,11 @@ function printImplementedTrait(trait: TraitImplBlock): Lines[] {
|
|
|
208
217
|
|
|
209
218
|
const head = `impl ${trait.traitName} for ${trait.structName}`;
|
|
210
219
|
|
|
211
|
-
const
|
|
212
|
-
|
|
220
|
+
const assocTypes = (Array.isArray(trait.assocType) ? trait.assocType : [trait.assocType ?? ''])
|
|
221
|
+
.map(assocType => assocType.trim())
|
|
222
|
+
.filter(assocType => assocType.length > 0);
|
|
223
|
+
|
|
224
|
+
const assocTypeLines = assocTypes.length > 0 ? [...assocTypes.map(assocType => TAB + assocType), ''] : [];
|
|
213
225
|
|
|
214
226
|
const functionLines = trait.functions.map(fn => printFunction(fn));
|
|
215
227
|
|
|
@@ -234,14 +246,14 @@ function printImplementedTrait(trait: TraitImplBlock): Lines[] {
|
|
|
234
246
|
|
|
235
247
|
function printFunction(fn: ContractFunction): Lines[] {
|
|
236
248
|
const head = `fn ${fn.name}`;
|
|
237
|
-
const args = fn.args
|
|
249
|
+
const args = printArguments(fn.args);
|
|
238
250
|
|
|
239
251
|
const codeLines = fn.codeBefore?.concat(fn.code) ?? fn.code;
|
|
240
252
|
for (let i = 0; i < codeLines.length; i++) {
|
|
241
253
|
const line = codeLines[i];
|
|
242
254
|
const shouldEndWithSemicolon = i < codeLines.length - 1 || fn.returns === undefined;
|
|
243
255
|
if (line !== undefined && line.length > 0) {
|
|
244
|
-
if (shouldEndWithSemicolon &&
|
|
256
|
+
if (shouldEndWithSemicolon && needsSemicolon(line)) {
|
|
245
257
|
codeLines[i] += ';';
|
|
246
258
|
} else if (!shouldEndWithSemicolon && line.endsWith(';')) {
|
|
247
259
|
codeLines[i] = line.slice(0, line.length - 1);
|
|
@@ -284,17 +296,23 @@ function printConstructor(contract: Contract): Lines[] {
|
|
|
284
296
|
const head = 'fn __constructor';
|
|
285
297
|
const args = [getSelfArg(), ...contract.constructorArgs];
|
|
286
298
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
299
|
+
// Empty entries separate logical groups of statements. Routing them through
|
|
300
|
+
// `spaceBetween` yields blank lines with no trailing indentation.
|
|
301
|
+
const groups = contract.constructorCode.reduce<string[][]>(
|
|
302
|
+
(acc, line) => {
|
|
303
|
+
if (line.length === 0) {
|
|
304
|
+
acc.push([]);
|
|
305
|
+
} else {
|
|
306
|
+
acc[acc.length - 1]!.push(line);
|
|
307
|
+
}
|
|
308
|
+
return acc;
|
|
309
|
+
},
|
|
310
|
+
[[]],
|
|
297
311
|
);
|
|
312
|
+
|
|
313
|
+
const body = spaceBetween(...groups.map(group => withSemicolons(group)));
|
|
314
|
+
|
|
315
|
+
const constructor = printFunction2(true, head, printArguments(args), [], undefined, undefined, body);
|
|
298
316
|
return constructor;
|
|
299
317
|
} else {
|
|
300
318
|
return [];
|
|
@@ -328,11 +346,12 @@ function printFunction2(
|
|
|
328
346
|
|
|
329
347
|
if (args.length > 0) {
|
|
330
348
|
const formattedArgs = args.join(', ');
|
|
331
|
-
|
|
349
|
+
// Commented arguments cannot share a line, so they force the expanded form.
|
|
350
|
+
if (formattedArgs.length > 80 || args.some(isComment)) {
|
|
332
351
|
fn.push(accum);
|
|
333
352
|
accum = '';
|
|
334
353
|
// print each arg in a separate line
|
|
335
|
-
fn.push(args.map(arg => `${arg},`));
|
|
354
|
+
fn.push(args.map(arg => (isComment(arg) ? arg : `${arg},`)));
|
|
336
355
|
} else {
|
|
337
356
|
accum += `${formattedArgs}`;
|
|
338
357
|
}
|
|
@@ -364,13 +383,23 @@ function printArgument(arg: Argument): string {
|
|
|
364
383
|
}
|
|
365
384
|
}
|
|
366
385
|
|
|
386
|
+
// Expands each argument into its comment lines followed by the argument itself.
|
|
387
|
+
// `printFunction2` recognizes the comment entries and leaves them untouched.
|
|
388
|
+
function printArguments(args: Argument[]): string[] {
|
|
389
|
+
return args.flatMap(arg => [...(arg.comment ?? []).map(line => `// ${line}`), printArgument(arg)]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isComment(line: string): boolean {
|
|
393
|
+
return line.startsWith('//');
|
|
394
|
+
}
|
|
395
|
+
|
|
367
396
|
function printTopLevelDeclarations(contract: Contract): Lines[] {
|
|
368
397
|
if (contract.topLevelDeclarations.length === 0) return [];
|
|
369
398
|
return spaceBetween(...contract.topLevelDeclarations);
|
|
370
399
|
}
|
|
371
400
|
|
|
372
401
|
function printDocumentations(documentations: string[]): string[] {
|
|
373
|
-
return documentations.map(documentation => `//! ${documentation}`);
|
|
402
|
+
return documentations.map(documentation => (documentation.length > 0 ? `//! ${documentation}` : '//!'));
|
|
374
403
|
}
|
|
375
404
|
|
|
376
405
|
function printMetadata(contract: Contract) {
|
|
@@ -27,6 +27,7 @@ const toCargoPath = (inputPath: string) => inputPath.replace(/\\/g, '/');
|
|
|
27
27
|
const localDependencies = [
|
|
28
28
|
['stellar-tokens', 'packages/tokens'],
|
|
29
29
|
['stellar-access', 'packages/access'],
|
|
30
|
+
['stellar-accounts', 'packages/accounts'],
|
|
30
31
|
['stellar-contract-utils', 'packages/contract-utils'],
|
|
31
32
|
['stellar-governance', 'packages/governance'],
|
|
32
33
|
['stellar-macros', 'packages/macros'],
|
package/src/zip-rust.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import JSZip from 'jszip';
|
|
2
|
+
import { defaults as accountDefaults } from './account';
|
|
2
3
|
import type { GenericOptions } from './build-generic';
|
|
3
4
|
import type { Contract } from './contract';
|
|
4
5
|
import { printContract, removeCreateLevelAttributes } from './print';
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
contractOptionsToContractName,
|
|
7
8
|
createRustLibFile,
|
|
8
9
|
getRequiredLibDependencies,
|
|
10
|
+
printAccountRustTest,
|
|
9
11
|
printContractCargo,
|
|
10
12
|
printRustNameTest,
|
|
11
13
|
printVaultRustTest,
|
|
@@ -31,6 +33,19 @@ const gitIgnore = `\
|
|
|
31
33
|
target/
|
|
32
34
|
`;
|
|
33
35
|
|
|
36
|
+
// Most contracts get the generic name assertion; kinds whose constructor takes
|
|
37
|
+
// more than plain addresses need a template that can satisfy it.
|
|
38
|
+
const printTest = (c: Contract, opts: GenericOptions) => {
|
|
39
|
+
switch (opts?.kind) {
|
|
40
|
+
case 'Account':
|
|
41
|
+
return printAccountRustTest(c, opts.policy ?? accountDefaults.policy);
|
|
42
|
+
case 'Vault':
|
|
43
|
+
return printVaultRustTest(c);
|
|
44
|
+
default:
|
|
45
|
+
return printRustNameTest(c);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
34
49
|
export const createRustZipEnvironment = (c: Contract, opts: GenericOptions) => {
|
|
35
50
|
const zip = new JSZip();
|
|
36
51
|
|
|
@@ -38,10 +53,7 @@ export const createRustZipEnvironment = (c: Contract, opts: GenericOptions) => {
|
|
|
38
53
|
const requiredLibDeps = getRequiredLibDependencies(c);
|
|
39
54
|
|
|
40
55
|
zip.file(`contracts/${contractName}/src/contract.rs`, removeCreateLevelAttributes(printContract(c)));
|
|
41
|
-
zip.file(
|
|
42
|
-
`contracts/${contractName}/src/test.rs`,
|
|
43
|
-
opts?.kind === 'Vault' ? printVaultRustTest(c) : printRustNameTest(c),
|
|
44
|
-
);
|
|
56
|
+
zip.file(`contracts/${contractName}/src/test.rs`, printTest(c, opts));
|
|
45
57
|
zip.file(`contracts/${contractName}/src/lib.rs`, createRustLibFile);
|
|
46
58
|
zip.file(`contracts/${contractName}/Cargo.toml`, printContractCargo(contractName, requiredLibDeps));
|
|
47
59
|
zip.file('Cargo.toml', workspaceCargo(requiredLibDeps));
|
package/src/zip-shared.ts
CHANGED
|
@@ -101,9 +101,240 @@ fn initial_state() {
|
|
|
101
101
|
`;
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
+
// A smart account contacts its policy and verifier contracts while it is being
|
|
105
|
+
// constructed: `add_context_rule` installs every policy and canonicalizes every
|
|
106
|
+
// external signer key through its verifier. A test that registers the account
|
|
107
|
+
// therefore needs real contracts at those addresses, so the mocks below wrap the
|
|
108
|
+
// library's own logic, the same way the reference examples do.
|
|
109
|
+
const accountPolicies = {
|
|
110
|
+
'simple-threshold': {
|
|
111
|
+
struct: 'MockSimpleThresholdPolicy',
|
|
112
|
+
module: 'simple_threshold',
|
|
113
|
+
params: 'SimpleThresholdAccountParams',
|
|
114
|
+
},
|
|
115
|
+
'weighted-threshold': {
|
|
116
|
+
struct: 'MockWeightedThresholdPolicy',
|
|
117
|
+
module: 'weighted_threshold',
|
|
118
|
+
params: 'WeightedThresholdAccountParams',
|
|
119
|
+
},
|
|
120
|
+
} as const;
|
|
121
|
+
|
|
122
|
+
export type AccountPolicy = keyof typeof accountPolicies | false;
|
|
123
|
+
|
|
124
|
+
// How many of each signer type the generated test deploys with. `weights` must
|
|
125
|
+
// have one entry per assembled signer, so the counts drive that too.
|
|
126
|
+
const testSignerCounts = {
|
|
127
|
+
delegated_signers: 2,
|
|
128
|
+
ed25519_keys: 1,
|
|
129
|
+
webauthn_keys: 1,
|
|
130
|
+
} as const;
|
|
131
|
+
|
|
132
|
+
const testConstructorArgValues: Record<string, (signerCount: number) => string> = {
|
|
133
|
+
delegated_signers: () => 'Vec::from_array(&env, [Address::generate(&env), Address::generate(&env)])',
|
|
134
|
+
ed25519_verifier: () => 'ed25519_verifier',
|
|
135
|
+
ed25519_keys: () => 'Vec::from_array(&env, [Bytes::from_slice(&env, &[1u8; 32])])',
|
|
136
|
+
webauthn_verifier: () => 'webauthn_verifier',
|
|
137
|
+
// A WebAuthn key is the 65-byte secp256r1 public key followed by an optional
|
|
138
|
+
// credential id; canonicalization reads the first 65 bytes.
|
|
139
|
+
webauthn_keys: () => 'Vec::from_array(&env, [Bytes::from_slice(&env, &[2u8; 65])])',
|
|
140
|
+
weights: signerCount => `Vec::from_array(&env, [${Array<string>(signerCount).fill('1u32').join(', ')}])`,
|
|
141
|
+
threshold: () => '1u32',
|
|
142
|
+
threshold_policy: () => 'policy',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const MOCK_ED25519_VERIFIER = `
|
|
146
|
+
// Minimal Ed25519 verifier contract, for testing purposes only: a real
|
|
147
|
+
// deployment passes the address of a verifier contract it trusts.
|
|
148
|
+
#[contract]
|
|
149
|
+
pub struct MockEd25519Verifier;
|
|
150
|
+
|
|
151
|
+
#[contractimpl]
|
|
152
|
+
impl Verifier for MockEd25519Verifier {
|
|
153
|
+
type KeyData = BytesN<32>;
|
|
154
|
+
type SigData = BytesN<64>;
|
|
155
|
+
|
|
156
|
+
fn verify(
|
|
157
|
+
e: &Env,
|
|
158
|
+
signature_payload: Bytes,
|
|
159
|
+
key_data: BytesN<32>,
|
|
160
|
+
sig_data: BytesN<64>,
|
|
161
|
+
) -> bool {
|
|
162
|
+
ed25519::verify(e, &signature_payload, &key_data, &sig_data)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn canonicalize_key(e: &Env, key_data: BytesN<32>) -> Bytes {
|
|
166
|
+
ed25519::canonicalize_key(e, &key_data)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn batch_canonicalize_key(e: &Env, keys_data: Vec<BytesN<32>>) -> Vec<Bytes> {
|
|
170
|
+
ed25519::batch_canonicalize_key(e, &keys_data)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
const MOCK_WEBAUTHN_VERIFIER = `
|
|
176
|
+
// Minimal WebAuthn (passkey) verifier contract, for testing purposes only: a
|
|
177
|
+
// real deployment passes the address of a verifier contract it trusts.
|
|
178
|
+
#[contract]
|
|
179
|
+
pub struct MockWebauthnVerifier;
|
|
180
|
+
|
|
181
|
+
#[contractimpl]
|
|
182
|
+
impl Verifier for MockWebauthnVerifier {
|
|
183
|
+
type KeyData = Bytes;
|
|
184
|
+
type SigData = Bytes;
|
|
185
|
+
|
|
186
|
+
fn verify(e: &Env, signature_payload: Bytes, key_data: Bytes, sig_data: Bytes) -> bool {
|
|
187
|
+
let sig_struct =
|
|
188
|
+
WebAuthnSigData::from_xdr(e, &sig_data).expect("WebAuthnSigData with correct format");
|
|
189
|
+
let pub_key: BytesN<65> =
|
|
190
|
+
extract_from_bytes(e, &key_data, 0..65).expect("65-byte public key to be extracted");
|
|
191
|
+
|
|
192
|
+
webauthn::verify(e, &signature_payload, &pub_key, &sig_struct)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn canonicalize_key(e: &Env, key_data: Bytes) -> Bytes {
|
|
196
|
+
webauthn::canonicalize_key(e, &key_data)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn batch_canonicalize_key(e: &Env, keys_data: Vec<Bytes>) -> Vec<Bytes> {
|
|
200
|
+
webauthn::batch_canonicalize_key(e, &keys_data)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
|
|
205
|
+
const printMockPolicyContract = (policy: keyof typeof accountPolicies) => {
|
|
206
|
+
const { struct, module, params } = accountPolicies[policy];
|
|
207
|
+
|
|
208
|
+
return `
|
|
209
|
+
// Minimal policy contract, for testing purposes only: the account installs its
|
|
210
|
+
// policies during construction, so the test needs one deployed at that address.
|
|
211
|
+
// A real deployment passes the address of a policy contract it trusts.
|
|
212
|
+
#[contract]
|
|
213
|
+
pub struct ${struct};
|
|
214
|
+
|
|
215
|
+
#[contractimpl]
|
|
216
|
+
impl Policy for ${struct} {
|
|
217
|
+
type AccountParams = ${module}::${params};
|
|
218
|
+
|
|
219
|
+
fn enforce(
|
|
220
|
+
e: &Env,
|
|
221
|
+
context: Context,
|
|
222
|
+
authenticated_signers: Vec<Signer>,
|
|
223
|
+
context_rule: ContextRule,
|
|
224
|
+
smart_account: Address,
|
|
225
|
+
) {
|
|
226
|
+
${module}::enforce(e, &context, &authenticated_signers, &context_rule, &smart_account)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn install(
|
|
230
|
+
e: &Env,
|
|
231
|
+
install_params: Self::AccountParams,
|
|
232
|
+
context_rule: ContextRule,
|
|
233
|
+
smart_account: Address,
|
|
234
|
+
) {
|
|
235
|
+
${module}::install(e, &install_params, &context_rule, &smart_account)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fn uninstall(e: &Env, context_rule: ContextRule, smart_account: Address) {
|
|
239
|
+
${module}::uninstall(e, &context_rule, &smart_account)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const printAccountRustTest = (c: Pick<Contract, 'constructorArgs' | 'name'>, policy: AccountPolicy) => {
|
|
246
|
+
const argNames = (c.constructorArgs || []).map(arg => arg.name);
|
|
247
|
+
const signerCount = Object.entries(testSignerCounts).reduce(
|
|
248
|
+
(total, [argName, count]) => (argNames.includes(argName) ? total + count : total),
|
|
249
|
+
0,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const hasEd25519 = argNames.includes('ed25519_keys');
|
|
253
|
+
const hasWebauthn = argNames.includes('webauthn_keys');
|
|
254
|
+
const hasPolicy = policy !== false;
|
|
255
|
+
|
|
256
|
+
// Every argument is spelled on its own line: the assembled tuple is otherwise
|
|
257
|
+
// far too long to read.
|
|
258
|
+
const registerArgs = argNames
|
|
259
|
+
.map(argName => ` ${testConstructorArgValues[argName]!(signerCount)},`)
|
|
260
|
+
.join('\n');
|
|
261
|
+
|
|
262
|
+
const hasMocks = hasEd25519 || hasWebauthn || hasPolicy;
|
|
263
|
+
|
|
264
|
+
const sorobanImports = [
|
|
265
|
+
...(hasPolicy ? ['auth::Context'] : []),
|
|
266
|
+
...(hasMocks ? ['contract', 'contractimpl'] : []),
|
|
267
|
+
'testutils::Address as _',
|
|
268
|
+
...(hasWebauthn ? ['xdr::FromXdr'] : []),
|
|
269
|
+
'Address',
|
|
270
|
+
...(hasEd25519 || hasWebauthn ? ['Bytes', 'BytesN'] : []),
|
|
271
|
+
'Env',
|
|
272
|
+
'Vec',
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
// Kept on separate lines so that enabling both verifiers does not import
|
|
276
|
+
// `Verifier` twice.
|
|
277
|
+
const libImports = [
|
|
278
|
+
...(hasPolicy
|
|
279
|
+
? [
|
|
280
|
+
`use stellar_accounts::policies::{ ${accountPolicies[policy].module}, Policy };`,
|
|
281
|
+
'use stellar_accounts::smart_account::{ ContextRule, Signer };',
|
|
282
|
+
]
|
|
283
|
+
: []),
|
|
284
|
+
...(hasEd25519 || hasWebauthn ? ['use stellar_accounts::verifiers::Verifier;'] : []),
|
|
285
|
+
...(hasEd25519 ? ['use stellar_accounts::verifiers::ed25519;'] : []),
|
|
286
|
+
...(hasWebauthn
|
|
287
|
+
? [
|
|
288
|
+
'use stellar_accounts::verifiers::utils::extract_from_bytes;',
|
|
289
|
+
'use stellar_accounts::verifiers::webauthn::{ self, WebAuthnSigData };',
|
|
290
|
+
]
|
|
291
|
+
: []),
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
const mockContracts = [
|
|
295
|
+
...(hasEd25519 ? [MOCK_ED25519_VERIFIER] : []),
|
|
296
|
+
...(hasWebauthn ? [MOCK_WEBAUTHN_VERIFIER] : []),
|
|
297
|
+
...(hasPolicy ? [printMockPolicyContract(policy)] : []),
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const registrations = [
|
|
301
|
+
...(hasEd25519 ? [' let ed25519_verifier = env.register(MockEd25519Verifier, ());'] : []),
|
|
302
|
+
...(hasWebauthn ? [' let webauthn_verifier = env.register(MockWebauthnVerifier, ());'] : []),
|
|
303
|
+
...(hasPolicy ? [` let policy = env.register(${accountPolicies[policy].struct}, ());`] : []),
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
return `#![cfg(test)]
|
|
307
|
+
|
|
308
|
+
extern crate std;
|
|
309
|
+
|
|
310
|
+
use soroban_sdk::{ ${sorobanImports.join(', ')} };
|
|
311
|
+
${libImports.map(libImport => `${libImport}\n`).join('')}
|
|
312
|
+
use crate::contract::{ ${c.name}, ${c.name}Client };
|
|
313
|
+
${mockContracts.join('')}
|
|
314
|
+
#[test]
|
|
315
|
+
fn initial_state() {
|
|
316
|
+
let env = Env::default();
|
|
317
|
+
${hasPolicy ? ' env.mock_all_auths();\n' : ''}
|
|
318
|
+
${registrations.map(registration => `${registration}\n`).join('')}
|
|
319
|
+
let contract_addr = env.register(
|
|
320
|
+
${c.name},
|
|
321
|
+
(
|
|
322
|
+
${registerArgs}
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
let client = ${c.name}Client::new(&env, &contract_addr);
|
|
326
|
+
|
|
327
|
+
assert_eq!(client.get_context_rules_count(), 1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Add more tests below
|
|
331
|
+
`;
|
|
332
|
+
};
|
|
333
|
+
|
|
104
334
|
const libDependencies = [
|
|
105
335
|
'stellar-tokens',
|
|
106
336
|
'stellar-access',
|
|
337
|
+
'stellar-accounts',
|
|
107
338
|
'stellar-contract-utils',
|
|
108
339
|
'stellar-governance',
|
|
109
340
|
'stellar-macros',
|