diff --git a/.changeset/plain-turkeys-film.md b/.changeset/plain-turkeys-film.md new file mode 100644 index 000000000..1f6f3735f --- /dev/null +++ b/.changeset/plain-turkeys-film.md @@ -0,0 +1,7 @@ +--- +'@openzeppelin/wizard-stellar': patch +'@openzeppelin/wizard-common': patch +'@openzeppelin/contracts-mcp': patch +--- + +Stellar: add an explicitImplementations flag that switches from using default_impl macro to explicit definitions diff --git a/packages/common/src/ai/descriptions/stellar.ts b/packages/common/src/ai/descriptions/stellar.ts index 752475567..cc151bcc2 100644 --- a/packages/common/src/ai/descriptions/stellar.ts +++ b/packages/common/src/ai/descriptions/stellar.ts @@ -12,6 +12,8 @@ export const stellarCommonDescriptions = { upgradeable: 'Whether the contract can be upgraded.', access: 'The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts.', + explicitImplementations: + 'Whether the contract should use explicit trait implementations instead of using the #[default_impl] macro to auto-generate trait method bodies.', }; export const stellarFungibleDescriptions = { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index e459a1099..153a0c91d 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -3,3 +3,4 @@ export * from './ai/descriptions/cairo'; export * from './ai/descriptions/solidity'; export * from './ai/descriptions/stellar'; export * from './ai/descriptions/stylus'; +export * from './utils/object'; diff --git a/packages/common/src/utils/object.ts b/packages/common/src/utils/object.ts new file mode 100644 index 000000000..8ac19dd90 --- /dev/null +++ b/packages/common/src/utils/object.ts @@ -0,0 +1,2 @@ +export const pickKeys = (obj: T, keys: K[]): Pick => + Object.fromEntries(keys.map(k => [k, obj[k]])) as Pick; diff --git a/packages/core/stellar/src/add-pausable.ts b/packages/core/stellar/src/add-pausable.ts index 1af5fd29d..d5cf968fd 100644 --- a/packages/core/stellar/src/add-pausable.ts +++ b/packages/core/stellar/src/add-pausable.ts @@ -1,13 +1,13 @@ import { getSelfArg } from './common-options'; import type { BaseFunction, ContractBuilder } from './contract'; import type { Access } from './set-access-control'; -import { requireAccessControl } from './set-access-control'; +import { DEFAULT_ACCESS_CONTROL, requireAccessControl } from './set-access-control'; import { defineFunctions } from './utils/define-functions'; -export function addPausable(c: ContractBuilder, access: Access) { +export function addPausable(c: ContractBuilder, access: Access, explicitImplementations: boolean) { c.addUseClause('stellar_contract_utils::pausable', 'self', { alias: 'pausable' }); c.addUseClause('stellar_contract_utils::pausable', 'Pausable'); - c.addUseClause('stellar_macros', 'default_impl'); + if (!explicitImplementations) c.addUseClause('stellar_macros', 'default_impl'); const pausableTrait = { traitName: 'Pausable', @@ -16,23 +16,38 @@ export function addPausable(c: ContractBuilder, access: Access) { section: 'Utils', }; - const pauseFn: BaseFunction = access === 'ownable' ? functions.pause_unused_caller : functions.pause; - const unpauseFn: BaseFunction = access === 'ownable' ? functions.unpause_unused_caller : functions.unpause; + const effectiveAccess = access === false ? DEFAULT_ACCESS_CONTROL : access; + const pauseFn: BaseFunction = effectiveAccess === 'ownable' ? functions.pause_unused_caller : functions.pause; + const unpauseFn: BaseFunction = effectiveAccess === 'ownable' ? functions.unpause_unused_caller : functions.unpause; c.addTraitFunction(pausableTrait, functions.paused); c.addTraitFunction(pausableTrait, pauseFn); c.addTraitFunction(pausableTrait, unpauseFn); - requireAccessControl(c, pausableTrait, pauseFn, access, { - useMacro: true, - role: 'pauser', - caller: 'caller', - }); + requireAccessControl( + c, + pausableTrait, + pauseFn, + effectiveAccess, + { + useMacro: true, + role: 'pauser', + caller: 'caller', + }, + explicitImplementations, + ); - requireAccessControl(c, pausableTrait, unpauseFn, access, { - useMacro: true, - role: 'pauser', - caller: 'caller', - }); + requireAccessControl( + c, + pausableTrait, + unpauseFn, + effectiveAccess, + { + useMacro: true, + role: 'pauser', + caller: 'caller', + }, + explicitImplementations, + ); } const functions = defineFunctions({ diff --git a/packages/core/stellar/src/add-upgradeable.ts b/packages/core/stellar/src/add-upgradeable.ts index da3c2dfe8..382ebb8cd 100644 --- a/packages/core/stellar/src/add-upgradeable.ts +++ b/packages/core/stellar/src/add-upgradeable.ts @@ -4,7 +4,7 @@ import { requireAccessControl } from './set-access-control'; import type { BaseFunction, ContractBuilder } from './contract'; import { defineFunctions } from './utils/define-functions'; -export function addUpgradeable(c: ContractBuilder, access: Access) { +export function addUpgradeable(c: ContractBuilder, access: Access, explicitImplementations: boolean) { const functions = defineFunctions({ _require_auth: { args: [getSelfArg(), { name: 'operator', type: '&Address' }], @@ -34,9 +34,16 @@ export function addUpgradeable(c: ContractBuilder, access: Access) { c.addTraitFunction(upgradeableTrait, upgradeFn); - requireAccessControl(c, upgradeableTrait, upgradeFn, access, { - useMacro: false, - role: 'upgrader', - caller: 'operator', - }); + requireAccessControl( + c, + upgradeableTrait, + upgradeFn, + access, + { + useMacro: false, + role: 'upgrader', + caller: 'operator', + }, + explicitImplementations, + ); } diff --git a/packages/core/stellar/src/common-options.ts b/packages/core/stellar/src/common-options.ts index f342cef7d..242c3d118 100644 --- a/packages/core/stellar/src/common-options.ts +++ b/packages/core/stellar/src/common-options.ts @@ -10,6 +10,7 @@ export const defaults: Required = { export const contractDefaults: Required = { ...defaults, access: false, + explicitImplementations: false, } as const; export interface CommonOptions { @@ -18,6 +19,7 @@ export interface CommonOptions { export interface CommonContractOptions extends CommonOptions { access?: Access; + explicitImplementations?: boolean; } export function withCommonDefaults(opts: CommonOptions): Required { @@ -30,6 +32,7 @@ export function withCommonContractDefaults(opts: CommonContractOptions): Require return { ...withCommonDefaults(opts), access: opts.access ?? contractDefaults.access, + explicitImplementations: opts.explicitImplementations ?? contractDefaults.explicitImplementations, }; } diff --git a/packages/core/stellar/src/contract.ts b/packages/core/stellar/src/contract.ts index 5dafa0d1b..af001963c 100644 --- a/packages/core/stellar/src/contract.ts +++ b/packages/core/stellar/src/contract.ts @@ -185,6 +185,10 @@ export class ContractBuilder implements Contract { } } + addTraitForEachFunctions(baseTrait: BaseTraitImplBlock, functions: Record) { + Object.values(functions).forEach(fn => this.addTraitFunction(baseTrait, fn)); + } + // used for adding a function to a trait implementation block addTraitFunction(baseTrait: BaseTraitImplBlock, fn: BaseFunction): ContractFunction { const t = this.addTraitImplBlock(baseTrait); @@ -241,6 +245,11 @@ export class ContractBuilder implements Contract { existingFn.tags = [...(existingFn.tags ?? []), tag]; } + setFunctionCode(fn: BaseFunction, code: string[], baseTrait?: BaseTraitImplBlock): void { + const existingFn = this.getOrCreateFunction(fn, baseTrait); + existingFn.code = [...code]; + } + addConstructorArgument(arg: Argument): void { for (const existingArg of this.constructorArgs) { if (existingArg.name == arg.name) { diff --git a/packages/core/stellar/src/fungible.test.ts b/packages/core/stellar/src/fungible.test.ts index 269b51cbb..92449a54b 100644 --- a/packages/core/stellar/src/fungible.test.ts +++ b/packages/core/stellar/src/fungible.test.ts @@ -98,6 +98,10 @@ testFungible('fungible full - complex name', { upgradeable: true, }); +testFungible('fungible explicit trait implementations', { + explicitImplementations: true, +}); + testAPIEquivalence('fungible API default'); testAPIEquivalence('fungible API basic', { name: 'CustomToken', symbol: 'CTK' }); diff --git a/packages/core/stellar/src/fungible.test.ts.md b/packages/core/stellar/src/fungible.test.ts.md index 466c2739c..2e5f1ed3e 100644 --- a/packages/core/stellar/src/fungible.test.ts.md +++ b/packages/core/stellar/src/fungible.test.ts.md @@ -124,12 +124,12 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - fn pause(e: &Env, caller: Address) {␊ + fn pause(e: &Env, _caller: Address) {␊ pausable::pause(e);␊ }␊ ␊ #[only_owner]␊ - fn unpause(e: &Env, caller: Address) {␊ + fn unpause(e: &Env, _caller: Address) {␊ pausable::unpause(e);␊ }␊ }␊ @@ -208,12 +208,12 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - fn pause(e: &Env, caller: Address) {␊ + fn pause(e: &Env, _caller: Address) {␊ pausable::pause(e);␊ }␊ ␊ #[only_owner]␊ - fn unpause(e: &Env, caller: Address) {␊ + fn unpause(e: &Env, _caller: Address) {␊ pausable::unpause(e);␊ }␊ }␊ @@ -362,7 +362,7 @@ Generated by [AVA](https://avajs.dev). // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ #![no_std]␊ ␊ - use soroban_sdk::{Address, contract, contractimpl, Env, String};␊ + use soroban_sdk::{Address, contract, contractimpl, Env, String, Symbol};␊ use stellar_access::access_control::{self as access_control, AccessControl};␊ use stellar_macros::default_impl;␊ use stellar_tokens::fungible::{Base, FungibleToken};␊ @@ -693,3 +693,66 @@ Generated by [AVA](https://avajs.dev). #[contractimpl]␊ impl Ownable for CustomToken {}␊ ` + +## fungible explicit trait implementations + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ + #![no_std]␊ + ␊ + use soroban_sdk::{Address, contract, contractimpl, Env, String};␊ + use stellar_tokens::fungible::{Base, FungibleToken};␊ + ␊ + #[contract]␊ + pub struct MyToken;␊ + ␊ + #[contractimpl]␊ + impl MyToken {␊ + pub fn __constructor(e: &Env) {␊ + Base::set_metadata(e, 18, String::from_str(e, "MyToken"), String::from_str(e, "MTK"));␊ + }␊ + }␊ + ␊ + #[contractimpl]␊ + impl FungibleToken for MyToken {␊ + type ContractType = Base;␊ + ␊ + fn total_supply(e: &Env) -> i128 {␊ + Self::ContractType::total_supply(e)␊ + }␊ + ␊ + fn balance(e: &Env, account: Address) -> i128 {␊ + Self::ContractType::balance(e, &account)␊ + }␊ + ␊ + fn allowance(e: &Env, owner: Address, spender: Address) -> i128 {␊ + Self::ContractType::allowance(e, &owner, &spender)␊ + }␊ + ␊ + fn transfer(e: &Env, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer(e, &from, &to, amount);␊ + }␊ + ␊ + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer_from(e, &spender, &from, &to, amount);␊ + }␊ + ␊ + fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) {␊ + Self::ContractType::approve(e, &owner, &spender, amount, live_until_ledger);␊ + }␊ + ␊ + fn decimals(e: &Env) -> u32 {␊ + Self::ContractType::decimals(e)␊ + }␊ + ␊ + fn name(e: &Env) -> String {␊ + Self::ContractType::name(e)␊ + }␊ + ␊ + fn symbol(e: &Env) -> String {␊ + Self::ContractType::symbol(e)␊ + }␊ + }␊ + ` diff --git a/packages/core/stellar/src/fungible.test.ts.snap b/packages/core/stellar/src/fungible.test.ts.snap index 8cc950058..a2d326d4f 100644 Binary files a/packages/core/stellar/src/fungible.test.ts.snap and b/packages/core/stellar/src/fungible.test.ts.snap differ diff --git a/packages/core/stellar/src/fungible.ts b/packages/core/stellar/src/fungible.ts index 083fe5105..ab4d20135 100644 --- a/packages/core/stellar/src/fungible.ts +++ b/packages/core/stellar/src/fungible.ts @@ -11,6 +11,7 @@ import { OptionsError } from './error'; import { contractDefaults as commonDefaults } from './common-options'; import { printContract } from './print'; import { toByteArray, toUint } from './utils/convert-strings'; +import { pickKeys } from '@openzeppelin/wizard-common'; export const defaults: Required = { name: 'MyToken', @@ -22,6 +23,7 @@ export const defaults: Required = { mintable: false, access: commonDefaults.access, info: commonDefaults.info, + explicitImplementations: commonDefaults.explicitImplementations, } as const; export function printFungible(opts: FungibleOptions = defaults): string { @@ -59,58 +61,68 @@ export function buildFungible(opts: FungibleOptions): ContractBuilder { const allOpts = withDefaults(opts); - addBase(c, toByteArray(allOpts.name), toByteArray(allOpts.symbol), allOpts.pausable); + addBase(c, toByteArray(allOpts.name), toByteArray(allOpts.symbol), allOpts.pausable, allOpts.explicitImplementations); if (allOpts.premint) { addPremint(c, allOpts.premint); } if (allOpts.pausable) { - addPausable(c, allOpts.access); + addPausable(c, allOpts.access, allOpts.explicitImplementations); } if (allOpts.upgradeable) { - addUpgradeable(c, allOpts.access); + addUpgradeable(c, allOpts.access, allOpts.explicitImplementations); } if (allOpts.burnable) { - addBurnable(c, allOpts.pausable); + addBurnable(c, allOpts.pausable, allOpts.explicitImplementations); } if (allOpts.mintable) { - addMintable(c, allOpts.access, allOpts.pausable); + addMintable(c, allOpts.access, allOpts.pausable, allOpts.explicitImplementations); } - setAccessControl(c, allOpts.access); + setAccessControl(c, allOpts.access, allOpts.explicitImplementations); setInfo(c, allOpts.info); return c; } -function addBase(c: ContractBuilder, name: string, symbol: string, pausable: boolean) { +function addBase( + c: ContractBuilder, + name: string, + symbol: string, + pausable: boolean, + explicitImplementations: boolean, +) { // Set metadata c.addConstructorCode(`Base::set_metadata(e, 18, String::from_str(e, "${name}"), String::from_str(e, "${symbol}"));`); // Set token functions c.addUseClause('stellar_tokens::fungible', 'Base'); c.addUseClause('stellar_tokens::fungible', 'FungibleToken'); - c.addUseClause('stellar_macros', 'default_impl'); + if (!explicitImplementations) c.addUseClause('stellar_macros', 'default_impl'); c.addUseClause('soroban_sdk', 'contract'); c.addUseClause('soroban_sdk', 'contractimpl'); c.addUseClause('soroban_sdk', 'String'); c.addUseClause('soroban_sdk', 'Env'); + if (explicitImplementations || pausable) { + c.addUseClause('soroban_sdk', 'Address'); + } const fungibleTokenTrait = { traitName: 'FungibleToken', structName: c.name, - tags: ['default_impl', 'contractimpl'], + tags: explicitImplementations ? ['contractimpl'] : ['default_impl', 'contractimpl'], assocType: 'type ContractType = Base;', }; c.addTraitImplBlock(fungibleTokenTrait); + if (explicitImplementations) c.addTraitForEachFunctions(fungibleTokenTrait, fungibleTokenTraitFunctions); + if (pausable) { - c.addUseClause('soroban_sdk', 'Address'); c.addUseClause('stellar_macros', 'when_not_paused'); c.addTraitFunction(fungibleTokenTrait, functions.transfer); @@ -121,7 +133,7 @@ function addBase(c: ContractBuilder, name: string, symbol: string, pausable: boo } } -function addMintable(c: ContractBuilder, access: Access, pausable: boolean) { +function addMintable(c: ContractBuilder, access: Access, pausable: boolean, explicitImplementations: boolean) { c.addUseClause('soroban_sdk', 'Address'); switch (access) { case false: @@ -129,7 +141,7 @@ function addMintable(c: ContractBuilder, access: Access, pausable: boolean) { case 'ownable': { c.addFreeFunction(functions.mint); - requireAccessControl(c, undefined, functions.mint, access); + requireAccessControl(c, undefined, functions.mint, access, undefined, explicitImplementations); if (pausable) { c.addFunctionTag(functions.mint, 'when_not_paused'); @@ -139,11 +151,18 @@ function addMintable(c: ContractBuilder, access: Access, pausable: boolean) { case 'roles': { c.addFreeFunction(functions.mint_with_caller); - requireAccessControl(c, undefined, functions.mint_with_caller, access, { - useMacro: true, - caller: 'caller', - role: 'minter', - }); + requireAccessControl( + c, + undefined, + functions.mint_with_caller, + access, + { + useMacro: true, + caller: 'caller', + role: 'minter', + }, + explicitImplementations, + ); if (pausable) { c.addFunctionTag(functions.mint_with_caller, 'when_not_paused'); @@ -157,7 +176,7 @@ function addMintable(c: ContractBuilder, access: Access, pausable: boolean) { } } -function addBurnable(c: ContractBuilder, pausable: boolean) { +function addBurnable(c: ContractBuilder, pausable: boolean, explicitImplementations: boolean) { c.addUseClause('stellar_tokens::fungible', 'burnable::FungibleBurnable'); c.addUseClause('soroban_sdk', 'Address'); @@ -176,7 +195,8 @@ function addBurnable(c: ContractBuilder, pausable: boolean) { c.addTraitFunction(fungibleBurnableTrait, functions.burn_from); c.addFunctionTag(functions.burn_from, 'when_not_paused', fungibleBurnableTrait); - } else { + } else if (explicitImplementations) c.addTraitForEachFunctions(fungibleBurnableTrait, fungibleBurnableFunctions); + else { // prepend '#[default_impl]' fungibleBurnableTrait.tags.unshift('default_impl'); c.addTraitImplBlock(fungibleBurnableTrait); @@ -335,3 +355,17 @@ export const functions = defineFunctions({ code: ['Base::mint(e, &account, amount);'], }, }); + +const fungibleTokenTraitFunctions = pickKeys(functions, [ + 'total_supply', + 'balance', + 'allowance', + 'transfer', + 'transfer_from', + 'approve', + 'decimals', + 'name', + 'symbol', +]); + +const fungibleBurnableFunctions = pickKeys(functions, ['burn', 'burn_from']); diff --git a/packages/core/stellar/src/generate/fungible.ts b/packages/core/stellar/src/generate/fungible.ts index 2a9aeb087..5fd2d53a3 100644 --- a/packages/core/stellar/src/generate/fungible.ts +++ b/packages/core/stellar/src/generate/fungible.ts @@ -15,6 +15,7 @@ const blueprint = { premint: ['1'], access: accessOptions, info: infoOptions, + explicitImplementations: booleans, }; export function* generateFungibleOptions(): Generator> { diff --git a/packages/core/stellar/src/generate/non-fungible.ts b/packages/core/stellar/src/generate/non-fungible.ts index b86fb5c86..96045eba1 100644 --- a/packages/core/stellar/src/generate/non-fungible.ts +++ b/packages/core/stellar/src/generate/non-fungible.ts @@ -18,6 +18,7 @@ const blueprint = { mintable: booleans, access: accessOptions, info: infoOptions, + explicitImplementations: booleans, }; export function* generateNonFungibleOptions(): Generator> { diff --git a/packages/core/stellar/src/generate/stablecoin.ts b/packages/core/stellar/src/generate/stablecoin.ts index 42717eff5..3818ff336 100644 --- a/packages/core/stellar/src/generate/stablecoin.ts +++ b/packages/core/stellar/src/generate/stablecoin.ts @@ -16,6 +16,7 @@ const blueprint = { access: accessOptions, limitations: limitationsOptions, info: infoOptions, + explicitImplementations: booleans, }; export function* generateStablecoinOptions(): Generator> { diff --git a/packages/core/stellar/src/non-fungible.test.ts b/packages/core/stellar/src/non-fungible.test.ts index 3293cac7a..f9e0b1b37 100644 --- a/packages/core/stellar/src/non-fungible.test.ts +++ b/packages/core/stellar/src/non-fungible.test.ts @@ -153,6 +153,10 @@ testNonFungible('non-fungible - complex name', { pausable: true, }); +testNonFungible('non-fungible explicit trait implementations', { + explicitImplementations: true, +}); + testNonFungible('non-fungible custom token uri', { tokenUri: 'https://example.com/nfts/', }); diff --git a/packages/core/stellar/src/non-fungible.test.ts.md b/packages/core/stellar/src/non-fungible.test.ts.md index 60e88769c..462dc1400 100644 --- a/packages/core/stellar/src/non-fungible.test.ts.md +++ b/packages/core/stellar/src/non-fungible.test.ts.md @@ -133,12 +133,12 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - fn pause(e: &Env, caller: Address) {␊ + fn pause(e: &Env, _caller: Address) {␊ pausable::pause(e);␊ }␊ ␊ #[only_owner]␊ - fn unpause(e: &Env, caller: Address) {␊ + fn unpause(e: &Env, _caller: Address) {␊ pausable::unpause(e);␊ }␊ }␊ @@ -220,12 +220,12 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - fn pause(e: &Env, caller: Address) {␊ + fn pause(e: &Env, _caller: Address) {␊ pausable::pause(e);␊ }␊ ␊ #[only_owner]␊ - fn unpause(e: &Env, caller: Address) {␊ + fn unpause(e: &Env, _caller: Address) {␊ pausable::unpause(e);␊ }␊ }␊ @@ -262,7 +262,7 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - pub fn mint(e: &Env, to: Address, token_id: u32, caller: Address) {␊ + pub fn mint(e: &Env, to: Address, token_id: u32) {␊ Base::mint(e, &to, token_id);␊ }␊ }␊ @@ -355,7 +355,7 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - pub fn batch_mint(e: &Env, to: Address, amount: u32, caller: Address) -> u32 {␊ + pub fn batch_mint(e: &Env, to: Address, amount: u32) -> u32 {␊ Consecutive::batch_mint(e, &to, amount)␊ }␊ }␊ @@ -413,7 +413,7 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - pub fn batch_mint(e: &Env, to: Address, amount: u32, caller: Address) -> u32 {␊ + pub fn batch_mint(e: &Env, to: Address, amount: u32) -> u32 {␊ Consecutive::batch_mint(e, &to, amount)␊ }␊ }␊ @@ -476,7 +476,7 @@ Generated by [AVA](https://avajs.dev). ␊ #[when_not_paused]␊ #[only_owner]␊ - pub fn batch_mint(e: &Env, to: Address, amount: u32, caller: Address) -> u32 {␊ + pub fn batch_mint(e: &Env, to: Address, amount: u32) -> u32 {␊ Consecutive::batch_mint(e, &to, amount)␊ }␊ }␊ @@ -515,12 +515,12 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - fn pause(e: &Env, caller: Address) {␊ + fn pause(e: &Env, _caller: Address) {␊ pausable::pause(e);␊ }␊ ␊ #[only_owner]␊ - fn unpause(e: &Env, caller: Address) {␊ + fn unpause(e: &Env, _caller: Address) {␊ pausable::unpause(e);␊ }␊ }␊ @@ -562,7 +562,7 @@ Generated by [AVA](https://avajs.dev). ␊ #[when_not_paused]␊ #[only_owner]␊ - pub fn batch_mint(e: &Env, to: Address, amount: u32, caller: Address) -> u32 {␊ + pub fn batch_mint(e: &Env, to: Address, amount: u32) -> u32 {␊ Consecutive::batch_mint(e, &to, amount)␊ }␊ }␊ @@ -614,12 +614,12 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - fn pause(e: &Env, caller: Address) {␊ + fn pause(e: &Env, _caller: Address) {␊ pausable::pause(e);␊ }␊ ␊ #[only_owner]␊ - fn unpause(e: &Env, caller: Address) {␊ + fn unpause(e: &Env, _caller: Address) {␊ pausable::unpause(e);␊ }␊ }␊ @@ -909,6 +909,86 @@ Generated by [AVA](https://avajs.dev). impl Ownable for CustomToken {}␊ ` +## non-fungible explicit trait implementations + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ + #![no_std]␊ + ␊ + use soroban_sdk::{Address, contract, contractimpl, Env, String};␊ + use stellar_tokens::non_fungible::{Base, ContractOverrides, NonFungibleToken};␊ + ␊ + #[contract]␊ + pub struct MyToken;␊ + ␊ + #[contractimpl]␊ + impl MyToken {␊ + pub fn __constructor(e: &Env) {␊ + let uri = String::from_str(e, "https://www.mytoken.com");␊ + let name = String::from_str(e, "MyToken");␊ + let symbol = String::from_str(e, "MTK");␊ + Base::set_metadata(e, uri, name, symbol);␊ + }␊ + }␊ + ␊ + #[contractimpl]␊ + impl NonFungibleToken for MyToken {␊ + type ContractType = Base;␊ + ␊ + fn balance(e: &Env, owner: Address) -> u32 {␊ + Self::ContractType::balance(e, &owner)␊ + }␊ + ␊ + fn owner_of(e: &Env, token_id: u32) -> Address {␊ + Self::ContractType::owner_of(e, token_id)␊ + }␊ + ␊ + fn transfer(e: &Env, from: Address, to: Address, token_id: u32) {␊ + Self::ContractType::transfer(e, &from, &to, token_id);␊ + }␊ + ␊ + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, token_id: u32) {␊ + Self::ContractType::transfer_from(e, &spender, &from, &to, token_id);␊ + }␊ + ␊ + fn approve(␊ + e: &Env,␊ + approver: Address,␊ + approved: Address,␊ + token_id: u32,␊ + live_until_ledger: u32,␊ + ) {␊ + Self::ContractType::approve(e, &approver, &approved, token_id, live_until_ledger);␊ + }␊ + ␊ + fn approve_for_all(e: &Env, owner: Address, operator: Address, live_until_ledger: u32) {␊ + Self::ContractType::approve_for_all(e, &owner, &operator, live_until_ledger);␊ + }␊ + ␊ + fn get_approved(e: &Env, token_id: u32) -> Option
{␊ + Self::ContractType::get_approved(e, token_id)␊ + }␊ + ␊ + fn is_approved_for_all(e: &Env, owner: Address, operator: Address) -> bool {␊ + Self::ContractType::is_approved_for_all(e, &owner, &operator)␊ + }␊ + ␊ + fn name(e: &Env) -> String {␊ + Self::ContractType::name(e)␊ + }␊ + ␊ + fn symbol(e: &Env) -> String {␊ + Self::ContractType::symbol(e)␊ + }␊ + ␊ + fn token_uri(e: &Env, token_id: u32) -> String {␊ + Self::ContractType::token_uri(e, token_id)␊ + }␊ + }␊ + ` + ## non-fungible custom token uri > Snapshot 1 diff --git a/packages/core/stellar/src/non-fungible.test.ts.snap b/packages/core/stellar/src/non-fungible.test.ts.snap index 52c1cee76..7c8e15e56 100644 Binary files a/packages/core/stellar/src/non-fungible.test.ts.snap and b/packages/core/stellar/src/non-fungible.test.ts.snap differ diff --git a/packages/core/stellar/src/non-fungible.ts b/packages/core/stellar/src/non-fungible.ts index 1859626f6..91899cc52 100644 --- a/packages/core/stellar/src/non-fungible.ts +++ b/packages/core/stellar/src/non-fungible.ts @@ -1,6 +1,6 @@ import type { Contract } from './contract'; import { ContractBuilder } from './contract'; -import { type Access, requireAccessControl, setAccessControl } from './set-access-control'; +import { type Access, DEFAULT_ACCESS_CONTROL, requireAccessControl, setAccessControl } from './set-access-control'; import { addPausable } from './add-pausable'; import { addUpgradeable } from './add-upgradeable'; import { defineFunctions } from './utils/define-functions'; @@ -12,6 +12,7 @@ import { OptionsError } from './error'; import { contractDefaults as commonDefaults } from './common-options'; import { printContract } from './print'; import { toByteArray } from './utils/convert-strings'; +import { pickKeys } from '@openzeppelin/wizard-common'; export const defaults: Required = { name: 'MyToken', @@ -26,6 +27,7 @@ export const defaults: Required = { sequential: false, access: commonDefaults.access, // TODO: Determine whether Access Control options should be visible in the UI before they are implemented as modules info: commonDefaults.info, + explicitImplementations: commonDefaults.explicitImplementations, } as const; export function printNonFungible(opts: NonFungibleOptions = defaults): string { @@ -90,39 +92,60 @@ export function buildNonFungible(opts: NonFungibleOptions): Contract { throw new OptionsError(errors); } - addBase(c, toByteArray(allOpts.name), toByteArray(allOpts.symbol), toByteArray(allOpts.tokenUri), allOpts.pausable); + addBase( + c, + toByteArray(allOpts.name), + toByteArray(allOpts.symbol), + toByteArray(allOpts.tokenUri), + allOpts.pausable, + allOpts.explicitImplementations, + ); if (allOpts.pausable) { - addPausable(c, allOpts.access); + addPausable(c, allOpts.access, allOpts.explicitImplementations); } if (allOpts.upgradeable) { - addUpgradeable(c, allOpts.access); + addUpgradeable(c, allOpts.access, allOpts.explicitImplementations); } if (allOpts.burnable) { - addBurnable(c, allOpts.pausable); + addBurnable(c, allOpts.pausable, allOpts.explicitImplementations); } if (allOpts.enumerable) { - addEnumerable(c); + addEnumerable(c, allOpts.explicitImplementations); } if (allOpts.consecutive) { - addConsecutive(c, allOpts.pausable, allOpts.access); + addConsecutive(c, allOpts.pausable, allOpts.access, allOpts.explicitImplementations); } if (allOpts.mintable) { - addMintable(c, allOpts.enumerable, allOpts.pausable, allOpts.sequential, allOpts.access); + addMintable( + c, + allOpts.enumerable, + allOpts.pausable, + allOpts.sequential, + allOpts.access, + allOpts.explicitImplementations, + ); } - setAccessControl(c, allOpts.access); + setAccessControl(c, allOpts.access, allOpts.explicitImplementations); setInfo(c, allOpts.info); return c; } -function addBase(c: ContractBuilder, name: string, symbol: string, tokenUri: string, pausable: boolean) { +function addBase( + c: ContractBuilder, + name: string, + symbol: string, + tokenUri: string, + pausable: boolean, + explicitImplementations: boolean, +) { // Set metadata c.addConstructorCode(`let uri = String::from_str(e, "${tokenUri}");`); c.addConstructorCode(`let name = String::from_str(e, "${name}");`); @@ -132,21 +155,25 @@ function addBase(c: ContractBuilder, name: string, symbol: string, tokenUri: str // Set token functions c.addUseClause('stellar_tokens::non_fungible', 'Base'); c.addUseClause('stellar_tokens::non_fungible', 'NonFungibleToken'); - c.addUseClause('stellar_macros', 'default_impl'); + if (explicitImplementations) c.addUseClause('stellar_tokens::non_fungible', 'ContractOverrides'); + if (!explicitImplementations) c.addUseClause('stellar_macros', 'default_impl'); c.addUseClause('soroban_sdk', 'contract'); c.addUseClause('soroban_sdk', 'contractimpl'); c.addUseClause('soroban_sdk', 'String'); c.addUseClause('soroban_sdk', 'Env'); + if (explicitImplementations || pausable) c.addUseClause('soroban_sdk', 'Address'); const nonFungibleTokenTrait = { traitName: 'NonFungibleToken', structName: c.name, - tags: ['default_impl', 'contractimpl'], + tags: explicitImplementations ? ['contractimpl'] : ['default_impl', 'contractimpl'], assocType: 'type ContractType = Base;', }; c.addTraitImplBlock(nonFungibleTokenTrait); + if (explicitImplementations) c.addTraitForEachFunctions(nonFungibleTokenTrait, nonFungibleTokenTraitFunctions); + if (pausable) { c.addUseClause('stellar_macros', 'when_not_paused'); @@ -158,7 +185,7 @@ function addBase(c: ContractBuilder, name: string, symbol: string, tokenUri: str } } -function addBurnable(c: ContractBuilder, pausable: boolean) { +function addBurnable(c: ContractBuilder, pausable: boolean, explicitImplementations: boolean) { c.addUseClause('stellar_tokens::non_fungible', 'burnable::NonFungibleBurnable'); const nonFungibleBurnableTrait = { @@ -176,32 +203,40 @@ function addBurnable(c: ContractBuilder, pausable: boolean) { c.addTraitFunction(nonFungibleBurnableTrait, burnableFunctions.burn_from); c.addFunctionTag(burnableFunctions.burn_from, 'when_not_paused', nonFungibleBurnableTrait); - } else { + } else if (explicitImplementations) + c.addTraitForEachFunctions(nonFungibleBurnableTrait, nonFungibleBurnableFunctions); + else { // prepend '#[default_impl]' nonFungibleBurnableTrait.tags.unshift('default_impl'); c.addTraitImplBlock(nonFungibleBurnableTrait); } } -function addEnumerable(c: ContractBuilder) { +function addEnumerable(c: ContractBuilder, explicitImplementations: boolean) { c.addUseClause('stellar_tokens::non_fungible', 'enumerable::{NonFungibleEnumerable, Enumerable}'); - c.addUseClause('stellar_macros', 'default_impl'); + if (!explicitImplementations) c.addUseClause('stellar_macros', 'default_impl'); + if (explicitImplementations) c.addUseClause('soroban_sdk', 'Address'); const nonFungibleEnumerableTrait = { traitName: 'NonFungibleEnumerable', structName: c.name, - tags: ['default_impl', 'contractimpl'], + tags: explicitImplementations ? ['contractimpl'] : ['default_impl', 'contractimpl'], section: 'Extensions', }; - c.addTraitImplBlock(nonFungibleEnumerableTrait); + if (explicitImplementations) + c.addTraitForEachFunctions(nonFungibleEnumerableTrait, nonFungibleEnumerableTraitFunctions); + else { + c.addTraitImplBlock(nonFungibleEnumerableTrait); + } c.overrideAssocType('NonFungibleToken', 'type ContractType = Enumerable;'); } -function addConsecutive(c: ContractBuilder, pausable: boolean, access: Access) { +function addConsecutive(c: ContractBuilder, pausable: boolean, access: Access, explicitImplementations: boolean) { c.addUseClause('stellar_tokens::non_fungible', 'consecutive::{NonFungibleConsecutive, Consecutive}'); c.addUseClause('soroban_sdk', 'Address'); + const effectiveAccess = access === false ? DEFAULT_ACCESS_CONTROL : access; const nonFungibleConsecutiveTrait = { traitName: 'NonFungibleConsecutive', structName: c.name, @@ -213,45 +248,64 @@ function addConsecutive(c: ContractBuilder, pausable: boolean, access: Access) { c.overrideAssocType('NonFungibleToken', 'type ContractType = Consecutive;'); - const mintFn = access === 'ownable' ? consecutiveFunctions.batch_mint : consecutiveFunctions.batch_mint_with_caller; + const mintFn = + effectiveAccess === 'ownable' ? consecutiveFunctions.batch_mint : consecutiveFunctions.batch_mint_with_caller; c.addFreeFunction(mintFn); if (pausable) { c.addFunctionTag(mintFn, 'when_not_paused'); } - requireAccessControl(c, undefined, mintFn, access, { - useMacro: true, - role: 'minter', - caller: 'caller', - }); + requireAccessControl( + c, + undefined, + mintFn, + effectiveAccess, + { + useMacro: true, + role: 'minter', + caller: 'caller', + }, + explicitImplementations, + ); } -function addMintable(c: ContractBuilder, enumerable: boolean, pausable: boolean, sequential: boolean, access: Access) { +function addMintable( + c: ContractBuilder, + enumerable: boolean, + pausable: boolean, + sequential: boolean, + access: Access, + explicitImplementations: boolean, +) { c.addUseClause('soroban_sdk', 'Address'); const accessProps = { useMacro: true, role: 'minter', caller: 'caller' }; + const effectiveAccess = access === false ? DEFAULT_ACCESS_CONTROL : access; let mintFn; if (enumerable) { if (sequential) { mintFn = - access === 'ownable' ? enumerableFunctions.sequential_mint : enumerableFunctions.sequential_mint_with_caller; + effectiveAccess === 'ownable' + ? enumerableFunctions.sequential_mint + : enumerableFunctions.sequential_mint_with_caller; } else { mintFn = - access === 'ownable' + effectiveAccess === 'ownable' ? enumerableFunctions.non_sequential_mint : enumerableFunctions.non_sequential_mint_with_caller; } } else { if (sequential) { - mintFn = access === 'ownable' ? baseFunctions.sequential_mint : baseFunctions.sequential_mint_with_caller; + mintFn = + effectiveAccess === 'ownable' ? baseFunctions.sequential_mint : baseFunctions.sequential_mint_with_caller; } else { - mintFn = access === 'ownable' ? baseFunctions.mint : baseFunctions.mint_with_caller; + mintFn = effectiveAccess === 'ownable' ? baseFunctions.mint : baseFunctions.mint_with_caller; } } c.addFreeFunction(mintFn); - requireAccessControl(c, undefined, mintFn, access, accessProps); + requireAccessControl(c, undefined, mintFn, effectiveAccess, accessProps, explicitImplementations); if (pausable) { c.addFunctionTag(mintFn, 'when_not_paused'); @@ -361,6 +415,20 @@ const baseFunctions = defineFunctions({ }, }); +const nonFungibleTokenTraitFunctions = pickKeys(baseFunctions, [ + 'balance', + 'owner_of', + 'transfer', + 'transfer_from', + 'approve', + 'approve_for_all', + 'get_approved', + 'is_approved_for_all', + 'name', + 'symbol', + 'token_uri', +]); + const burnableFunctions = defineFunctions({ burn: { args: [getSelfArg(), { name: 'from', type: 'Address' }, { name: 'token_id', type: 'u32' }], @@ -377,11 +445,23 @@ const burnableFunctions = defineFunctions({ }, }); +const nonFungibleBurnableFunctions = pickKeys(burnableFunctions, ['burn', 'burn_from']); + const enumerableFunctions = defineFunctions({ total_supply: { args: [getSelfArg()], returns: 'u32', - code: ['non_fungible::enumerable::Enumerable::total_supply(e)'], + code: ['Enumerable::total_supply(e)'], + }, + get_owner_token_id: { + args: [getSelfArg(), { name: 'owner', type: 'Address' }, { name: 'index', type: 'u32' }], + returns: 'u32', + code: ['Enumerable::get_owner_token_id(e, &owner, index)'], + }, + get_token_id: { + args: [getSelfArg(), { name: 'index', type: 'u32' }], + returns: 'u32', + code: ['Enumerable::get_token_id(e, index)'], }, non_sequential_mint: { name: 'mint', @@ -410,12 +490,18 @@ const enumerableFunctions = defineFunctions({ }, }); +const nonFungibleEnumerableTraitFunctions = pickKeys(enumerableFunctions, [ + 'total_supply', + 'get_owner_token_id', + 'get_token_id', +]); + const consecutiveFunctions = defineFunctions({ batch_mint: { name: 'batch_mint', args: [getSelfArg(), { name: 'to', type: 'Address' }, { name: 'amount', type: 'u32' }], returns: 'u32', - code: ['Consecutive::batch_mint(e, &to, amount);'], + code: ['Consecutive::batch_mint(e, &to, amount)'], }, batch_mint_with_caller: { name: 'batch_mint', @@ -426,6 +512,6 @@ const consecutiveFunctions = defineFunctions({ { name: 'caller', type: 'Address' }, ], returns: 'u32', - code: ['Consecutive::batch_mint(e, &to, amount);'], + code: ['Consecutive::batch_mint(e, &to, amount)'], }, }); diff --git a/packages/core/stellar/src/set-access-control.ts b/packages/core/stellar/src/set-access-control.ts index 9725c58ed..c192dc08a 100644 --- a/packages/core/stellar/src/set-access-control.ts +++ b/packages/core/stellar/src/set-access-control.ts @@ -1,4 +1,6 @@ import type { BaseFunction, BaseTraitImplBlock, ContractBuilder } from './contract'; +import { defineFunctions } from './utils/define-functions'; +import { getSelfArg } from './common-options'; export const accessOptions = [false, 'ownable', 'roles'] as const; export const DEFAULT_ACCESS_CONTROL = 'ownable'; @@ -13,7 +15,7 @@ export type AccessProps = { /** * Sets access control for the contract via constructor args. */ -export function setAccessControl(c: ContractBuilder, access: Access): void { +export function setAccessControl(c: ContractBuilder, access: Access, explicitImplementations = false): void { switch (access) { case false: break; @@ -27,10 +29,14 @@ export function setAccessControl(c: ContractBuilder, access: Access): void { const ownableTrait = { traitName: 'Ownable', structName: c.name, - tags: ['default_impl', 'contractimpl'], + tags: explicitImplementations ? ['contractimpl'] : ['default_impl', 'contractimpl'], section: 'Utils', }; - c.addTraitImplBlock(ownableTrait); + if (explicitImplementations) { + c.addTraitForEachFunctions(ownableTrait, ownableFunctions); + } else { + c.addTraitImplBlock(ownableTrait); + } c.addConstructorArgument({ name: 'owner', type: 'Address' }); c.addConstructorCode('ownable::set_owner(e, &owner);'); @@ -39,16 +45,21 @@ export function setAccessControl(c: ContractBuilder, access: Access): void { } case 'roles': { c.addUseClause('soroban_sdk', 'Address'); + c.addUseClause('soroban_sdk', 'Symbol'); c.addUseClause('stellar_access::access_control', 'self', { alias: 'access_control' }); c.addUseClause('stellar_access::access_control', 'AccessControl'); - const accessControltrait = { + const accessControlTrait = { traitName: 'AccessControl', structName: c.name, - tags: ['default_impl', 'contractimpl'], + tags: explicitImplementations ? ['contractimpl'] : ['default_impl', 'contractimpl'], section: 'Utils', }; - c.addTraitImplBlock(accessControltrait); + if (explicitImplementations) { + c.addTraitForEachFunctions(accessControlTrait, accessControlFunctions); + } else { + c.addTraitImplBlock(accessControlTrait); + } c.addConstructorArgument({ name: 'admin', type: 'Address' }); c.addConstructorCode('access_control::set_admin(e, &admin);'); @@ -70,11 +81,12 @@ export function requireAccessControl( fn: BaseFunction, access: Access, accessProps: AccessProps = { useMacro: true }, + explicitImplementations = false, ): void { if (access === false) { access = DEFAULT_ACCESS_CONTROL; } - setAccessControl(c, access); + setAccessControl(c, access, explicitImplementations); switch (access) { case 'ownable': { @@ -123,3 +135,89 @@ export function requireAccessControl( } } } + +const ownableFunctions = defineFunctions({ + get_owner: { + args: [getSelfArg()], + returns: 'Option
', + code: ['ownable::get_owner(e)'], + }, + transfer_ownership: { + args: [getSelfArg(), { name: 'new_owner', type: 'Address' }, { name: 'live_until_ledger', type: 'u32' }], + code: ['ownable::transfer_ownership(e, &new_owner, live_until_ledger);'], + }, + accept_ownership: { + args: [getSelfArg()], + code: ['ownable::accept_ownership(e);'], + }, + renounce_ownership: { + args: [getSelfArg()], + code: ['ownable::renounce_ownership(e);'], + }, +}); + +const accessControlFunctions = defineFunctions({ + has_role: { + args: [getSelfArg(), { name: 'account', type: 'Address' }, { name: 'role', type: 'Symbol' }], + returns: 'Option', + code: ['access_control::has_role(e, &account, &role)'], + }, + get_role_member_count: { + args: [getSelfArg(), { name: 'role', type: 'Symbol' }], + returns: 'u32', + code: ['access_control::get_role_member_count(e, &role)'], + }, + get_role_member: { + args: [getSelfArg(), { name: 'role', type: 'Symbol' }, { name: 'index', type: 'u32' }], + returns: 'Address', + code: ['access_control::get_role_member(e, &role, index)'], + }, + get_role_admin: { + args: [getSelfArg(), { name: 'role', type: 'Symbol' }], + returns: 'Option', + code: ['access_control::get_role_admin(e, &role)'], + }, + get_admin: { + args: [getSelfArg()], + returns: 'Option
', + code: ['access_control::get_admin(e)'], + }, + grant_role: { + args: [ + getSelfArg(), + { name: 'caller', type: 'Address' }, + { name: 'account', type: 'Address' }, + { name: 'role', type: 'Symbol' }, + ], + code: ['access_control::grant_role(e, &caller, &account, &role);'], + }, + revoke_role: { + args: [ + getSelfArg(), + { name: 'caller', type: 'Address' }, + { name: 'account', type: 'Address' }, + { name: 'role', type: 'Symbol' }, + ], + code: ['access_control::revoke_role(e, &caller, &account, &role);'], + }, + renounce_role: { + args: [getSelfArg(), { name: 'caller', type: 'Address' }, { name: 'role', type: 'Symbol' }], + code: ['access_control::renounce_role(e, &caller, &role);'], + }, + transfer_admin_role: { + args: [getSelfArg(), { name: 'new_admin', type: 'Address' }, { name: 'live_until_ledger', type: 'u32' }], + code: ['access_control::transfer_admin_role(e, &new_admin, live_until_ledger);'], + }, + accept_admin_transfer: { + args: [getSelfArg()], + code: ['access_control::accept_admin_transfer(e);'], + }, + set_role_admin: { + args: [getSelfArg(), { name: 'role', type: 'Symbol' }, { name: 'admin_role', type: 'Symbol' }], + code: ['access_control::set_role_admin(e, &role, &admin_role);'], + }, + renounce_admin: { + args: [getSelfArg()], + code: ['access_control::renounce_admin(e);'], + }, +}); diff --git a/packages/core/stellar/src/stablecoin.test.ts b/packages/core/stellar/src/stablecoin.test.ts index 96feae9ff..c20012e3a 100644 --- a/packages/core/stellar/src/stablecoin.test.ts +++ b/packages/core/stellar/src/stablecoin.test.ts @@ -78,6 +78,16 @@ testStablecoin('stablecoin blocklist', { limitations: 'blocklist', }); +testStablecoin('stablecoin allowlist explicit trait implementations', { + limitations: 'allowlist', + explicitImplementations: true, +}); + +testStablecoin('stablecoin blocklist explicit trait implementations', { + limitations: 'blocklist', + explicitImplementations: true, +}); + testStablecoin('stablecoin full - ownable, allowlist', { premint: '2000', access: 'ownable', @@ -123,6 +133,10 @@ testStablecoin('stablecoin full - complex name', { pausable: true, }); +testStablecoin('stablecoin explicit trait implementations', { + explicitImplementations: true, +}); + testAPIEquivalence('stablecoin API default'); testAPIEquivalence('stablecoin API basic', { name: 'CustomToken', symbol: 'CTK' }); diff --git a/packages/core/stellar/src/stablecoin.test.ts.md b/packages/core/stellar/src/stablecoin.test.ts.md index 496964c3e..bf2717b43 100644 --- a/packages/core/stellar/src/stablecoin.test.ts.md +++ b/packages/core/stellar/src/stablecoin.test.ts.md @@ -124,12 +124,12 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - fn pause(e: &Env, caller: Address) {␊ + fn pause(e: &Env, _caller: Address) {␊ pausable::pause(e);␊ }␊ ␊ #[only_owner]␊ - fn unpause(e: &Env, caller: Address) {␊ + fn unpause(e: &Env, _caller: Address) {␊ pausable::unpause(e);␊ }␊ }␊ @@ -208,12 +208,12 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ #[only_owner]␊ - fn pause(e: &Env, caller: Address) {␊ + fn pause(e: &Env, _caller: Address) {␊ pausable::pause(e);␊ }␊ ␊ #[only_owner]␊ - fn unpause(e: &Env, caller: Address) {␊ + fn unpause(e: &Env, _caller: Address) {␊ pausable::unpause(e);␊ }␊ }␊ @@ -362,7 +362,7 @@ Generated by [AVA](https://avajs.dev). // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ #![no_std]␊ ␊ - use soroban_sdk::{Address, contract, contractimpl, Env, String};␊ + use soroban_sdk::{Address, contract, contractimpl, Env, String, Symbol};␊ use stellar_access::access_control::{self as access_control, AccessControl};␊ use stellar_macros::default_impl;␊ use stellar_tokens::fungible::{Base, FungibleToken};␊ @@ -520,6 +520,230 @@ Generated by [AVA](https://avajs.dev). impl Ownable for MyStablecoin {}␊ ` +## stablecoin allowlist explicit trait implementations + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ + #![no_std]␊ + ␊ + use soroban_sdk::{Address, contract, contractimpl, Env, String};␊ + use stellar_access::ownable::{self as ownable, Ownable};␊ + use stellar_macros::only_owner;␊ + use stellar_tokens::fungible::{␊ + allowlist::{AllowList, FungibleAllowList}, Base, FungibleToken␊ + };␊ + ␊ + #[contract]␊ + pub struct MyStablecoin;␊ + ␊ + #[contractimpl]␊ + impl MyStablecoin {␊ + pub fn __constructor(e: &Env, owner: Address) {␊ + Base::set_metadata(e, 18, String::from_str(e, "MyStablecoin"), String::from_str(e, "MST"));␊ + ownable::set_owner(e, &owner);␊ + }␊ + }␊ + ␊ + #[contractimpl]␊ + impl FungibleToken for MyStablecoin {␊ + type ContractType = AllowList;␊ + ␊ + fn total_supply(e: &Env) -> i128 {␊ + Base::total_supply(e)␊ + }␊ + ␊ + fn balance(e: &Env, account: Address) -> i128 {␊ + Base::balance(e, &account)␊ + }␊ + ␊ + fn allowance(e: &Env, owner: Address, spender: Address) -> i128 {␊ + Base::allowance(e, &owner, &spender)␊ + }␊ + ␊ + fn transfer(e: &Env, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer(e, &from, &to, amount);␊ + }␊ + ␊ + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer_from(e, &spender, &from, &to, amount);␊ + }␊ + ␊ + fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) {␊ + Self::ContractType::approve(e, &owner, &spender, amount, live_until_ledger);␊ + }␊ + ␊ + fn decimals(e: &Env) -> u32 {␊ + Base::decimals(e)␊ + }␊ + ␊ + fn name(e: &Env) -> String {␊ + Base::name(e)␊ + }␊ + ␊ + fn symbol(e: &Env) -> String {␊ + Base::symbol(e)␊ + }␊ + }␊ + ␊ + //␊ + // Extensions␊ + //␊ + ␊ + #[contractimpl]␊ + impl FungibleAllowList for MyStablecoin {␊ + fn allowed(e: &Env, account: Address) -> bool {␊ + AllowList::allowed(e, &account)␊ + }␊ + ␊ + #[only_owner]␊ + fn allow_user(e: &Env, user: Address, operator: Address) {␊ + AllowList::allow_user(e, &user);␊ + }␊ + ␊ + #[only_owner]␊ + fn disallow_user(e: &Env, user: Address, operator: Address) {␊ + AllowList::disallow_user(e, &user);␊ + }␊ + }␊ + ␊ + //␊ + // Utils␊ + //␊ + ␊ + #[contractimpl]␊ + impl Ownable for MyStablecoin {␊ + fn get_owner(e: &Env) -> Option
{␊ + ownable::get_owner(e)␊ + }␊ + ␊ + fn transfer_ownership(e: &Env, new_owner: Address, live_until_ledger: u32) {␊ + ownable::transfer_ownership(e, &new_owner, live_until_ledger);␊ + }␊ + ␊ + fn accept_ownership(e: &Env) {␊ + ownable::accept_ownership(e);␊ + }␊ + ␊ + fn renounce_ownership(e: &Env) {␊ + ownable::renounce_ownership(e);␊ + }␊ + }␊ + ` + +## stablecoin blocklist explicit trait implementations + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ + #![no_std]␊ + ␊ + use soroban_sdk::{Address, contract, contractimpl, Env, String};␊ + use stellar_access::ownable::{self as ownable, Ownable};␊ + use stellar_macros::only_owner;␊ + use stellar_tokens::fungible::{␊ + Base, blocklist::{BlockList, FungibleBlockList}, FungibleToken␊ + };␊ + ␊ + #[contract]␊ + pub struct MyStablecoin;␊ + ␊ + #[contractimpl]␊ + impl MyStablecoin {␊ + pub fn __constructor(e: &Env, owner: Address) {␊ + Base::set_metadata(e, 18, String::from_str(e, "MyStablecoin"), String::from_str(e, "MST"));␊ + ownable::set_owner(e, &owner);␊ + }␊ + }␊ + ␊ + #[contractimpl]␊ + impl FungibleToken for MyStablecoin {␊ + type ContractType = BlockList;␊ + ␊ + fn total_supply(e: &Env) -> i128 {␊ + Base::total_supply(e)␊ + }␊ + ␊ + fn balance(e: &Env, account: Address) -> i128 {␊ + Base::balance(e, &account)␊ + }␊ + ␊ + fn allowance(e: &Env, owner: Address, spender: Address) -> i128 {␊ + Base::allowance(e, &owner, &spender)␊ + }␊ + ␊ + fn transfer(e: &Env, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer(e, &from, &to, amount);␊ + }␊ + ␊ + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer_from(e, &spender, &from, &to, amount);␊ + }␊ + ␊ + fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) {␊ + Self::ContractType::approve(e, &owner, &spender, amount, live_until_ledger);␊ + }␊ + ␊ + fn decimals(e: &Env) -> u32 {␊ + Base::decimals(e)␊ + }␊ + ␊ + fn name(e: &Env) -> String {␊ + Base::name(e)␊ + }␊ + ␊ + fn symbol(e: &Env) -> String {␊ + Base::symbol(e)␊ + }␊ + }␊ + ␊ + //␊ + // Extensions␊ + //␊ + ␊ + #[contractimpl]␊ + impl FungibleBlockList for MyStablecoin {␊ + fn blocked(e: &Env, account: Address) -> bool {␊ + BlockList::blocked(e, &account)␊ + }␊ + ␊ + #[only_owner]␊ + fn block_user(e: &Env, user: Address, operator: Address) {␊ + BlockList::block_user(e, &user);␊ + }␊ + ␊ + #[only_owner]␊ + fn unblock_user(e: &Env, user: Address, operator: Address) {␊ + BlockList::unblock_user(e, &user);␊ + }␊ + }␊ + ␊ + //␊ + // Utils␊ + //␊ + ␊ + #[contractimpl]␊ + impl Ownable for MyStablecoin {␊ + fn get_owner(e: &Env) -> Option
{␊ + ownable::get_owner(e)␊ + }␊ + ␊ + fn transfer_ownership(e: &Env, new_owner: Address, live_until_ledger: u32) {␊ + ownable::transfer_ownership(e, &new_owner, live_until_ledger);␊ + }␊ + ␊ + fn accept_ownership(e: &Env) {␊ + ownable::accept_ownership(e);␊ + }␊ + ␊ + fn renounce_ownership(e: &Env) {␊ + ownable::renounce_ownership(e);␊ + }␊ + }␊ + ` + ## stablecoin full - ownable, allowlist > Snapshot 1 @@ -1070,3 +1294,66 @@ Generated by [AVA](https://avajs.dev). #[contractimpl]␊ impl Ownable for CustomToken {}␊ ` + +## stablecoin explicit trait implementations + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ + #![no_std]␊ + ␊ + use soroban_sdk::{Address, contract, contractimpl, Env, String};␊ + use stellar_tokens::fungible::{Base, FungibleToken};␊ + ␊ + #[contract]␊ + pub struct MyStablecoin;␊ + ␊ + #[contractimpl]␊ + impl MyStablecoin {␊ + pub fn __constructor(e: &Env) {␊ + Base::set_metadata(e, 18, String::from_str(e, "MyStablecoin"), String::from_str(e, "MST"));␊ + }␊ + }␊ + ␊ + #[contractimpl]␊ + impl FungibleToken for MyStablecoin {␊ + type ContractType = Base;␊ + ␊ + fn total_supply(e: &Env) -> i128 {␊ + Self::ContractType::total_supply(e)␊ + }␊ + ␊ + fn balance(e: &Env, account: Address) -> i128 {␊ + Self::ContractType::balance(e, &account)␊ + }␊ + ␊ + fn allowance(e: &Env, owner: Address, spender: Address) -> i128 {␊ + Self::ContractType::allowance(e, &owner, &spender)␊ + }␊ + ␊ + fn transfer(e: &Env, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer(e, &from, &to, amount);␊ + }␊ + ␊ + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer_from(e, &spender, &from, &to, amount);␊ + }␊ + ␊ + fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) {␊ + Self::ContractType::approve(e, &owner, &spender, amount, live_until_ledger);␊ + }␊ + ␊ + fn decimals(e: &Env) -> u32 {␊ + Self::ContractType::decimals(e)␊ + }␊ + ␊ + fn name(e: &Env) -> String {␊ + Self::ContractType::name(e)␊ + }␊ + ␊ + fn symbol(e: &Env) -> String {␊ + Self::ContractType::symbol(e)␊ + }␊ + }␊ + ` diff --git a/packages/core/stellar/src/stablecoin.test.ts.snap b/packages/core/stellar/src/stablecoin.test.ts.snap index 93b9ab4da..d085af474 100644 Binary files a/packages/core/stellar/src/stablecoin.test.ts.snap and b/packages/core/stellar/src/stablecoin.test.ts.snap differ diff --git a/packages/core/stellar/src/stablecoin.ts b/packages/core/stellar/src/stablecoin.ts index 1dc50f830..6e217b141 100644 --- a/packages/core/stellar/src/stablecoin.ts +++ b/packages/core/stellar/src/stablecoin.ts @@ -48,13 +48,18 @@ export function buildStablecoin(opts: StablecoinOptions): Contract { const c = buildFungible(allOpts); if (allOpts.limitations) { - addLimitations(c, allOpts.access, allOpts.limitations); + addLimitations(c, allOpts.access, allOpts.limitations, allOpts.explicitImplementations); } return c; } -function addLimitations(c: ContractBuilder, access: Access, mode: 'allowlist' | 'blocklist') { +function addLimitations( + c: ContractBuilder, + access: Access, + mode: 'allowlist' | 'blocklist', + explicitImplementations: boolean, +) { const type = mode === 'allowlist'; const limitationsTrait = { @@ -72,6 +77,8 @@ function addLimitations(c: ContractBuilder, access: Access, mode: 'allowlist' | c.overrideAssocType('FungibleToken', 'type ContractType = BlockList;'); } + if (explicitImplementations) overrideFungibleReadonlyFunctionsWithBase(c); + const [getterFn, addFn, removeFn] = type ? [functions.allowed, functions.allow_user, functions.disallow_user] : [functions.blocked, functions.block_user, functions.unblock_user]; @@ -85,10 +92,31 @@ function addLimitations(c: ContractBuilder, access: Access, mode: 'allowlist' | }; c.addTraitFunction(limitationsTrait, addFn); - requireAccessControl(c, limitationsTrait, addFn, access, accessProps); + requireAccessControl(c, limitationsTrait, addFn, access, accessProps, explicitImplementations); c.addTraitFunction(limitationsTrait, removeFn); - requireAccessControl(c, limitationsTrait, removeFn, access, accessProps); + requireAccessControl(c, limitationsTrait, removeFn, access, accessProps, explicitImplementations); +} + +function overrideFungibleReadonlyFunctionsWithBase(c: ContractBuilder) { + const fungibleTokenTrait = { + traitName: 'FungibleToken', + structName: c.name, + tags: [], + }; + + const overrides: Array<[(typeof fungibleFunctions)[keyof typeof fungibleFunctions], string[]]> = [ + [fungibleFunctions.total_supply, ['Base::total_supply(e)']], + [fungibleFunctions.balance, ['Base::balance(e, &account)']], + [fungibleFunctions.allowance, ['Base::allowance(e, &owner, &spender)']], + [fungibleFunctions.decimals, ['Base::decimals(e)']], + [fungibleFunctions.name, ['Base::name(e)']], + [fungibleFunctions.symbol, ['Base::symbol(e)']], + ]; + + for (const [fn, code] of overrides) { + c.setFunctionCode(fn, code, fungibleTokenTrait); + } } const functions = { diff --git a/packages/core/stellar/src/utils/compile-test.ts b/packages/core/stellar/src/utils/compile-test.ts index 9d1781e87..490d8ea37 100644 --- a/packages/core/stellar/src/utils/compile-test.ts +++ b/packages/core/stellar/src/utils/compile-test.ts @@ -6,7 +6,7 @@ import { exec } from 'child_process'; import type { GenericOptions } from '../build-generic'; import type { Contract } from '../contract'; import { assertLayout, snapshotZipContents, expandPathsFromFilesPaths, extractPackage } from './zip-test'; -import { mkdtemp, rm } from 'fs/promises'; +import { mkdtemp, rm, mkdir } from 'fs/promises'; import { contractOptionsToContractName } from '../zip-shared'; import { zipRustProject } from '../zip-rust'; @@ -29,7 +29,6 @@ export const withTemporaryFolderDo = (...testFunctionArguments: Args) => async (test: ExecutionContext) => { const temporaryFolder = await mkdtemp(path.join(tmpdir(), `compilation-test-${crypto.randomUUID()}`)); - try { await testFunction(...testFunctionArguments, test, temporaryFolder); } finally { @@ -37,33 +36,60 @@ export const withTemporaryFolderDo = } }; +const doRunRustCompilationTest = async ( + makeContract: MakeContract, + opts: GenericOptions, + testOptions: { snapshotResult: boolean }, + test: ExecutionContext, + folderPath: string, +) => { + test.timeout(3_000_000); + + await mkdir(folderPath, { recursive: true }); + + const scaffoldContractName = contractOptionsToContractName(opts?.kind || 'contract'); + + const expectedZipFiles = [ + `contracts/${scaffoldContractName}/src/contract.rs`, + `contracts/${scaffoldContractName}/src/test.rs`, + `contracts/${scaffoldContractName}/src/lib.rs`, + `contracts/${scaffoldContractName}/Cargo.toml`, + 'Cargo.toml', + 'README.md', + ]; + + const zip = await zipRustProject(makeContract(opts), opts); + + assertLayout(test, zip, expandPathsFromFilesPaths(expectedZipFiles)); + await extractPackage(zip, folderPath); + await runCargoTest(test, folderPath); + + if (testOptions.snapshotResult) await snapshotZipContents(test, zip, expectedZipFiles); +}; + export const runRustCompilationTest = withTemporaryFolderDo( async ( makeContract: MakeContract, opts: GenericOptions, - testOptions: { snapshotResult: boolean }, + testOptions: { snapshotResult: boolean; excludeExplicitTraitTest?: boolean }, test: ExecutionContext, folderPath: string, ) => { - test.timeout(3_000_000); - - const scaffoldContractName = contractOptionsToContractName(opts?.kind || 'contract'); - - const expectedZipFiles = [ - `contracts/${scaffoldContractName}/src/contract.rs`, - `contracts/${scaffoldContractName}/src/test.rs`, - `contracts/${scaffoldContractName}/src/lib.rs`, - `contracts/${scaffoldContractName}/Cargo.toml`, - 'Cargo.toml', - 'README.md', - ]; + await doRunRustCompilationTest(makeContract, opts, testOptions, test, `${folderPath}/default`); - const zip = await zipRustProject(makeContract(opts), opts); + const shouldBeExcludedOrHasAlreadyRun = testOptions.excludeExplicitTraitTest || opts.explicitImplementations; + if (shouldBeExcludedOrHasAlreadyRun) return; - assertLayout(test, zip, expandPathsFromFilesPaths(expectedZipFiles)); - await extractPackage(zip, folderPath); - await runCargoTest(test, folderPath); - - if (testOptions.snapshotResult) await snapshotZipContents(test, zip, expectedZipFiles); + try { + await doRunRustCompilationTest( + makeContract, + { ...opts, explicitImplementations: true }, + testOptions, + test, + `${folderPath}/explicit`, + ); + } catch (error) { + throw new Error(`EXPLICIT IMPLEMENTATION ERROR: ${error}`); + } }, ); diff --git a/packages/core/stellar/src/zip-rust.compile.test.ts.md b/packages/core/stellar/src/zip-rust.compile.test.ts.md index 8eecc5875..3dfbe5aa9 100644 --- a/packages/core/stellar/src/zip-rust.compile.test.ts.md +++ b/packages/core/stellar/src/zip-rust.compile.test.ts.md @@ -139,3 +139,177 @@ Generated by [AVA](https://avajs.dev). - See [Git installation guide](https://github.com/git-guides/install-git).␊ `, ] + +> Snapshot 2 + + [ + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ + ␊ + ␊ + use soroban_sdk::{Address, contract, contractimpl, Env, String};␊ + use stellar_tokens::fungible::{Base, burnable::FungibleBurnable, FungibleToken};␊ + ␊ + #[contract]␊ + pub struct MyToken;␊ + ␊ + #[contractimpl]␊ + impl MyToken {␊ + pub fn __constructor(e: &Env, recipient: Address) {␊ + Base::set_metadata(e, 18, String::from_str(e, "MyToken"), String::from_str(e, "MTK"));␊ + Base::mint(e, &recipient, 2000000000000000000000);␊ + }␊ + }␊ + ␊ + #[contractimpl]␊ + impl FungibleToken for MyToken {␊ + type ContractType = Base;␊ + ␊ + fn total_supply(e: &Env) -> i128 {␊ + Self::ContractType::total_supply(e)␊ + }␊ + ␊ + fn balance(e: &Env, account: Address) -> i128 {␊ + Self::ContractType::balance(e, &account)␊ + }␊ + ␊ + fn allowance(e: &Env, owner: Address, spender: Address) -> i128 {␊ + Self::ContractType::allowance(e, &owner, &spender)␊ + }␊ + ␊ + fn transfer(e: &Env, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer(e, &from, &to, amount);␊ + }␊ + ␊ + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) {␊ + Self::ContractType::transfer_from(e, &spender, &from, &to, amount);␊ + }␊ + ␊ + fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) {␊ + Self::ContractType::approve(e, &owner, &spender, amount, live_until_ledger);␊ + }␊ + ␊ + fn decimals(e: &Env) -> u32 {␊ + Self::ContractType::decimals(e)␊ + }␊ + ␊ + fn name(e: &Env) -> String {␊ + Self::ContractType::name(e)␊ + }␊ + ␊ + fn symbol(e: &Env) -> String {␊ + Self::ContractType::symbol(e)␊ + }␊ + }␊ + ␊ + //␊ + // Extensions␊ + //␊ + ␊ + #[contractimpl]␊ + impl FungibleBurnable for MyToken {␊ + fn burn(e: &Env, from: Address, amount: i128) {␊ + Base::burn(e, &from, amount);␊ + }␊ + ␊ + fn burn_from(e: &Env, spender: Address, from: Address, amount: i128) {␊ + Base::burn_from(e, &spender, &from, amount);␊ + }␊ + }␊ + `, + `#![cfg(test)]␊ + ␊ + extern crate std;␊ + ␊ + use soroban_sdk::{ testutils::Address as _, Address, Env, String };␊ + ␊ + use crate::contract::{ MyToken, MyTokenClient };␊ + ␊ + #[test]␊ + fn initial_state() {␊ + let env = Env::default();␊ + ␊ + let contract_addr = env.register(MyToken, (Address::generate(&env),));␊ + let client = MyTokenClient::new(&env, &contract_addr);␊ + ␊ + assert_eq!(client.name(), String::from_str(&env, "MyToken"));␊ + }␊ + ␊ + // Add more tests bellow␊ + `, + `#![no_std]␊ + #![allow(dead_code)]␊ + ␊ + mod contract;␊ + mod test;␊ + `, + `[package]␊ + name = "fungible-contract"␊ + edition.workspace = true␊ + license.workspace = true␊ + publish = false␊ + version.workspace = true␊ + ␊ + [package.metadata.stellar]␊ + cargo_inherit = true␊ + ␊ + [lib]␊ + crate-type = ["cdylib"]␊ + doctest = false␊ + ␊ + [dependencies]␊ + stellar-tokens = { workspace = true }␊ + stellar-access = { workspace = true }␊ + stellar-contract-utils = { workspace = true }␊ + stellar-macros = { workspace = true }␊ + soroban-sdk = { workspace = true }␊ + ␊ + [dev-dependencies]␊ + soroban-sdk = { workspace = true, features = ["testutils"] }␊ + `, + `[workspace]␊ + resolver = "2"␊ + members = ["contracts/*"]␊ + ␊ + [workspace.package]␊ + authors = []␊ + edition = "2021"␊ + license = "Apache-2.0"␊ + version = "0.0.1"␊ + ␊ + [workspace.dependencies]␊ + soroban-sdk = "22.0.8"␊ + stellar-tokens = "=0.4.1"␊ + stellar-access = "=0.4.1"␊ + stellar-contract-utils = "=0.4.1"␊ + stellar-macros = "=0.4.1"␊ + ␊ + ␊ + [profile.release]␊ + opt-level = "z"␊ + overflow-checks = true␊ + debug = 0␊ + strip = "symbols"␊ + debug-assertions = false␊ + panic = "abort"␊ + codegen-units = 1␊ + lto = true␊ + ␊ + [profile.release-with-logs]␊ + inherits = "release"␊ + debug-assertions = true␊ + `, + `# Sample Rust Contract Environment␊ + ␊ + This project demonstrates a basic Rust contract environment use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/) and a test for that contract. Make sure you have the required dependencies and keep building!␊ + ␊ + ## Go further␊ + ␊ + Continue your development journey with [Stellar CLI](https://github.com/stellar/stellar-cli).␊ + ␊ + ## Installing dependencies␊ + ␊ + - See [Rust and Stellar installation guide](https://developers.stellar.org/docs/build/smart-contracts/getting-started/setup).␊ + - See [Git installation guide](https://github.com/git-guides/install-git).␊ + `, + ] diff --git a/packages/core/stellar/src/zip-rust.compile.test.ts.snap b/packages/core/stellar/src/zip-rust.compile.test.ts.snap index d554d6bd2..28b5d7819 100644 Binary files a/packages/core/stellar/src/zip-rust.compile.test.ts.snap and b/packages/core/stellar/src/zip-rust.compile.test.ts.snap differ diff --git a/packages/mcp/src/solidity/tools/erc20.test.ts.md b/packages/mcp/src/solidity/tools/erc20.test.ts.md deleted file mode 100644 index f4bad6d43..000000000 --- a/packages/mcp/src/solidity/tools/erc20.test.ts.md +++ /dev/null @@ -1,15 +0,0 @@ -# Snapshot report for `src/solidity/tools/erc20.test.ts` - -The actual snapshot is saved in `erc20.test.ts.snap`. - -Generated by [AVA](https://avajs.dev). - -## all - -> Snapshot 1 - - `Invalid options␊ - ␊ - {␊ - "crossChainBridging": "Upgradeability is not currently supported with Cross-Chain Bridging"␊ - }` diff --git a/packages/mcp/src/solidity/tools/erc20.test.ts.snap b/packages/mcp/src/solidity/tools/erc20.test.ts.snap deleted file mode 100644 index f37a0240f..000000000 Binary files a/packages/mcp/src/solidity/tools/erc20.test.ts.snap and /dev/null differ diff --git a/packages/mcp/src/stellar/schemas.ts b/packages/mcp/src/stellar/schemas.ts index 5d667b03e..93174730e 100644 --- a/packages/mcp/src/stellar/schemas.ts +++ b/packages/mcp/src/stellar/schemas.ts @@ -24,6 +24,7 @@ function _typeAssertions() { export const commonSchema = { access: z.literal('ownable').or(z.literal('roles')).optional().describe(stellarCommonDescriptions.access), + explicitImplementations: z.boolean().optional().describe(stellarCommonDescriptions.explicitImplementations), upgradeable: z.boolean().optional().describe(stellarCommonDescriptions.upgradeable), info: z .object({ diff --git a/packages/mcp/src/stellar/tools/fungible.test.ts b/packages/mcp/src/stellar/tools/fungible.test.ts index 889707de8..a58252a5a 100644 --- a/packages/mcp/src/stellar/tools/fungible.test.ts +++ b/packages/mcp/src/stellar/tools/fungible.test.ts @@ -38,7 +38,7 @@ test('basic', async t => { await assertAPIEquivalence(t, params, fungible.print); }); -test('all', async t => { +test('all explicit', async t => { const params: DeepRequired> = { name: 'TestToken', symbol: 'TST', @@ -47,6 +47,7 @@ test('all', async t => { premint: '1000000', mintable: true, upgradeable: true, + explicitImplementations: true, access: 'ownable', info: { license: 'MIT', @@ -56,3 +57,23 @@ test('all', async t => { assertHasAllSupportedFields(t, params); await assertAPIEquivalence(t, params, fungible.print); }); + +test('all default', async t => { + const params: DeepRequired> = { + name: 'TestToken', + symbol: 'TST', + burnable: true, + pausable: true, + premint: '1000000', + mintable: true, + upgradeable: true, + access: 'ownable', + explicitImplementations: false, + info: { + license: 'MIT', + securityContact: 'security@contact.com', + }, + }; + assertHasAllSupportedFields(t, params); + await assertAPIEquivalence(t, params, fungible.print); +}); diff --git a/packages/mcp/src/stellar/tools/fungible.ts b/packages/mcp/src/stellar/tools/fungible.ts index c5fe304bd..afacdf9e8 100644 --- a/packages/mcp/src/stellar/tools/fungible.ts +++ b/packages/mcp/src/stellar/tools/fungible.ts @@ -10,7 +10,18 @@ export function registerStellarFungible(server: McpServer): RegisteredTool { 'stellar-fungible', makeDetailedPrompt(stellarPrompts.Fungible), fungibleSchema, - async ({ name, symbol, burnable, pausable, premint, mintable, upgradeable, access, info }) => { + async ({ + name, + symbol, + burnable, + pausable, + premint, + mintable, + upgradeable, + access, + info, + explicitImplementations, + }) => { const opts: FungibleOptions = { name, symbol, @@ -21,6 +32,7 @@ export function registerStellarFungible(server: McpServer): RegisteredTool { upgradeable, access, info, + explicitImplementations, }; return { content: [ diff --git a/packages/mcp/src/stellar/tools/non-fungible.test.ts b/packages/mcp/src/stellar/tools/non-fungible.test.ts index 59643e92d..fa575b1a8 100644 --- a/packages/mcp/src/stellar/tools/non-fungible.test.ts +++ b/packages/mcp/src/stellar/tools/non-fungible.test.ts @@ -39,7 +39,7 @@ test('basic', async t => { await assertAPIEquivalence(t, params, nonFungible.print); }); -test('all', async t => { +test('all default', async t => { const params: DeepRequired> = { name: 'TestToken', symbol: 'TST', @@ -52,6 +52,33 @@ test('all', async t => { mintable: true, sequential: true, access: 'ownable', + explicitImplementations: false, + info: { + license: 'MIT', + securityContact: 'security@contact.com', + }, + }; + assertHasAllSupportedFields(t, params); + + // Records an error in the snapshot, because some fields are incompatible with each other. + // This is ok, because we just need to check that all fields can be passed in. + await assertAPIEquivalence(t, params, nonFungible.print, true); +}); + +test('all explicit', async t => { + const params: DeepRequired> = { + name: 'TestToken', + symbol: 'TST', + tokenUri: 'https://example.com/nft/', + burnable: true, + enumerable: true, + consecutive: true, + pausable: true, + upgradeable: true, + mintable: true, + sequential: true, + explicitImplementations: true, + access: 'ownable', info: { license: 'MIT', securityContact: 'security@contact.com', diff --git a/packages/mcp/src/stellar/tools/non-fungible.test.ts.md b/packages/mcp/src/stellar/tools/non-fungible.test.ts.md index 8f8073967..f1baaba25 100644 --- a/packages/mcp/src/stellar/tools/non-fungible.test.ts.md +++ b/packages/mcp/src/stellar/tools/non-fungible.test.ts.md @@ -4,7 +4,20 @@ The actual snapshot is saved in `non-fungible.test.ts.snap`. Generated by [AVA](https://avajs.dev). -## all +## all default + +> Snapshot 1 + + `Invalid options␊ + ␊ + {␊ + "enumerable": "Enumerable cannot be used with Consecutive extension",␊ + "consecutive": "Consecutive cannot be used with Sequential minting",␊ + "mintable": "Mintable cannot be used with Consecutive extension",␊ + "sequential": "Sequential minting cannot be used with Consecutive extension"␊ + }` + +## all explicit > Snapshot 1 diff --git a/packages/mcp/src/stellar/tools/non-fungible.test.ts.snap b/packages/mcp/src/stellar/tools/non-fungible.test.ts.snap index 248ced064..a872ea677 100644 Binary files a/packages/mcp/src/stellar/tools/non-fungible.test.ts.snap and b/packages/mcp/src/stellar/tools/non-fungible.test.ts.snap differ diff --git a/packages/mcp/src/stellar/tools/non-fungible.ts b/packages/mcp/src/stellar/tools/non-fungible.ts index 9c120e39c..0b23d61a4 100644 --- a/packages/mcp/src/stellar/tools/non-fungible.ts +++ b/packages/mcp/src/stellar/tools/non-fungible.ts @@ -22,6 +22,7 @@ export function registerStellarNonFungible(server: McpServer): RegisteredTool { sequential, upgradeable, info, + explicitImplementations, }) => { const opts: NonFungibleOptions = { name, @@ -35,6 +36,7 @@ export function registerStellarNonFungible(server: McpServer): RegisteredTool { sequential, upgradeable, info, + explicitImplementations, }; return { content: [ diff --git a/packages/mcp/src/stellar/tools/stablecoin.test.ts b/packages/mcp/src/stellar/tools/stablecoin.test.ts index b338bca43..d4e81be06 100644 --- a/packages/mcp/src/stellar/tools/stablecoin.test.ts +++ b/packages/mcp/src/stellar/tools/stablecoin.test.ts @@ -38,7 +38,7 @@ test('basic', async t => { await assertAPIEquivalence(t, params, stablecoin.print); }); -test('all', async t => { +test('all default', async t => { const params: DeepRequired> = { name: 'TestToken', symbol: 'TST', @@ -49,6 +49,28 @@ test('all', async t => { upgradeable: true, access: 'ownable', limitations: 'allowlist', + explicitImplementations: false, + info: { + license: 'MIT', + securityContact: 'security@contact.com', + }, + }; + assertHasAllSupportedFields(t, params); + await assertAPIEquivalence(t, params, stablecoin.print); +}); + +test('all explicit', async t => { + const params: DeepRequired> = { + name: 'TestToken', + symbol: 'TST', + burnable: true, + pausable: true, + premint: '1000000', + mintable: true, + upgradeable: true, + access: 'ownable', + limitations: 'allowlist', + explicitImplementations: true, info: { license: 'MIT', securityContact: 'security@contact.com', diff --git a/packages/mcp/src/stellar/tools/stablecoin.ts b/packages/mcp/src/stellar/tools/stablecoin.ts index e132ee2d9..f5ef4a741 100644 --- a/packages/mcp/src/stellar/tools/stablecoin.ts +++ b/packages/mcp/src/stellar/tools/stablecoin.ts @@ -10,7 +10,19 @@ export function registerStellarStablecoin(server: McpServer): RegisteredTool { 'stellar-stablecoin', makeDetailedPrompt(stellarPrompts.Stablecoin), stablecoinSchema, - async ({ name, symbol, burnable, pausable, premint, mintable, upgradeable, access, limitations, info }) => { + async ({ + name, + symbol, + burnable, + pausable, + premint, + mintable, + upgradeable, + access, + limitations, + explicitImplementations, + info, + }) => { const opts: StablecoinOptions = { name, symbol, @@ -21,6 +33,7 @@ export function registerStellarStablecoin(server: McpServer): RegisteredTool { upgradeable, access, limitations, + explicitImplementations, info, }; return { diff --git a/packages/ui/api/ai-assistant/function-definitions/stellar-shared.ts b/packages/ui/api/ai-assistant/function-definitions/stellar-shared.ts index 6c030b1bc..c697d71aa 100644 --- a/packages/ui/api/ai-assistant/function-definitions/stellar-shared.ts +++ b/packages/ui/api/ai-assistant/function-definitions/stellar-shared.ts @@ -17,6 +17,11 @@ export const stellarCommonFunctionDescription = { description: stellarCommonDescriptions.upgradeable, }, + explicitImplementations: { + type: 'boolean', + description: stellarCommonDescriptions.explicitImplementations, + }, + info: { type: 'object', description: infoDescriptions.info, diff --git a/packages/ui/api/ai-assistant/function-definitions/stellar.ts b/packages/ui/api/ai-assistant/function-definitions/stellar.ts index bab3a484d..f7ba450d2 100644 --- a/packages/ui/api/ai-assistant/function-definitions/stellar.ts +++ b/packages/ui/api/ai-assistant/function-definitions/stellar.ts @@ -24,6 +24,7 @@ export const stellarFungibleAIFunctionDefinition = { 'mintable', 'access', 'info', + 'explicitImplementations', ]), premint: { type: 'string', @@ -49,6 +50,7 @@ export const stellarStablecoinAIFunctionDefinition = { 'mintable', 'access', 'info', + 'explicitImplementations', ]), limitations: { anyOf: [ @@ -86,6 +88,7 @@ export const stellarNonFungibleAIFunctionDefinition = { 'mintable', 'access', 'info', + 'explicitImplementations', ]), enumerable: { type: 'boolean', diff --git a/packages/ui/rollup.config.mjs b/packages/ui/rollup.config.mjs index 2f6eb05b4..09008d955 100644 --- a/packages/ui/rollup.config.mjs +++ b/packages/ui/rollup.config.mjs @@ -136,7 +136,7 @@ export default [ commonjs(), typescript({ - include: ['src/**/*.ts', '../core/*/src/**/*.ts'], + include: ['src/**/*.ts', '../core/*/src/**/*.ts', '../common/src/**/*.ts'], sourceMap: true, inlineSources: true, }), diff --git a/packages/ui/src/stellar/FungibleControls.svelte b/packages/ui/src/stellar/FungibleControls.svelte index 049766612..65e5e5e95 100644 --- a/packages/ui/src/stellar/FungibleControls.svelte +++ b/packages/ui/src/stellar/FungibleControls.svelte @@ -6,6 +6,7 @@ import AccessControlSection from './AccessControlSection.svelte'; import InfoSection from './InfoSection.svelte'; + import TraitImplementationSection from './TraitImplementationSection.svelte'; import { error } from '../common/error-tooltip'; export let opts: Required = { @@ -44,6 +45,8 @@ + +

Features

diff --git a/packages/ui/src/stellar/NonFungibleControls.svelte b/packages/ui/src/stellar/NonFungibleControls.svelte index 5cbc71e03..2c18782fe 100644 --- a/packages/ui/src/stellar/NonFungibleControls.svelte +++ b/packages/ui/src/stellar/NonFungibleControls.svelte @@ -7,6 +7,7 @@ import AccessControlSection from './AccessControlSection.svelte'; import InfoSection from './InfoSection.svelte'; import MintableSection from './MintableSection.svelte'; + import TraitImplementationSection from './TraitImplementationSection.svelte'; import { error } from '../common/error-tooltip'; export let opts: Required = { @@ -60,6 +61,8 @@
+ +

Features

diff --git a/packages/ui/src/stellar/StablecoinControls.svelte b/packages/ui/src/stellar/StablecoinControls.svelte index 392c37a46..c8c66dd6e 100644 --- a/packages/ui/src/stellar/StablecoinControls.svelte +++ b/packages/ui/src/stellar/StablecoinControls.svelte @@ -7,6 +7,7 @@ import AccessControlSection from './AccessControlSection.svelte'; import InfoSection from './InfoSection.svelte'; import ExpandableToggleRadio from '../common/ExpandableToggleRadio.svelte'; + import TraitImplementationSection from './TraitImplementationSection.svelte'; import { error } from '../common/error-tooltip'; export let opts: Required = { @@ -45,6 +46,8 @@
+ +

Features

diff --git a/packages/ui/src/stellar/TraitImplementationSection.svelte b/packages/ui/src/stellar/TraitImplementationSection.svelte new file mode 100644 index 000000000..49cf0bd88 --- /dev/null +++ b/packages/ui/src/stellar/TraitImplementationSection.svelte @@ -0,0 +1,46 @@ + + +
+

Trait Implementations

+

+ Whether the contract should use explicit trait implementations instead of the #[default_impl] macro to auto-generate + trait method bodies. +

+
+ + + +
+
+ + diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index f8fee0533..2b89b1b21 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -18,11 +18,12 @@ }, "include": [ "src/**/*", - "../core/*/src/**/*" + "../core/*/src/**/*", + "../common/src/**/*" ], "exclude": [ "node_modules/*", "__sapper__/*", "public/*" ] -} \ No newline at end of file +}