Skip to content
This repository was archived by the owner on Nov 27, 2024. It is now read-only.

Commit b7f76dc

Browse files
authored
feat: algokit generators support (#40)
* feat: algokit generators support
1 parent 9e110e7 commit b7f76dc

File tree

222 files changed

+6065
-341
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

222 files changed

+6065
-341
lines changed

.editorconfig

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
root=true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
end_of_line = lf
7+
insert_final_newline = true
8+
9+
[*.py]
10+
indent_size = 4

copier.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
_subdirectory: template_content
2+
_templates_suffix: ".jinja"
23

34
# questions
45
# project_name should never get prompted, AlgoKit should always pass it by convention

template_content/.algokit.toml.jinja

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[algokit]
2-
min_version = "v1.3.0b1"
2+
min_version = "v1.4.0"
33

44
[deploy]
55
{%- if deployment_language == 'python' %}
@@ -16,3 +16,7 @@ environment_secrets = [
1616

1717
[deploy.localnet]
1818
environment_secrets = []
19+
20+
[generate.smart_contract]
21+
description = "Adds new smart contract to existing project"
22+
path = ".algokit/generators/create_contract"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
_task:
2+
- "echo '==== Successfully initialized new smart contract 🚀 ===='"
3+
4+
contract_name:
5+
type: str
6+
help: Name of your new contract.
7+
placeholder: "my-new-contract"
8+
default: "my-new-contract"
9+
10+
_templates_suffix: ".j2"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import beaker
2+
import pyteal as pt
3+
{% if preset_name == 'starter' %}
4+
5+
app = beaker.Application("{{ contract_name }}")
6+
{% elif preset_name == 'production' -%}
7+
from algokit_utils import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME
8+
9+
app = beaker.Application("{{ contract_name }}")
10+
11+
12+
@app.update(authorize=beaker.Authorize.only_creator(), bare=True)
13+
def update() -> pt.Expr:
14+
return pt.Assert(
15+
pt.Tmpl.Int(UPDATABLE_TEMPLATE_NAME),
16+
comment="Check app is updatable",
17+
)
18+
19+
20+
@app.delete(authorize=beaker.Authorize.only_creator(), bare=True)
21+
def delete() -> pt.Expr:
22+
return pt.Assert(
23+
pt.Tmpl.Int(DELETABLE_TEMPLATE_NAME),
24+
comment="Check app is deletable",
25+
)
26+
{% endif %}
27+
28+
@app.external
29+
def hello(name: pt.abi.String, *, output: pt.abi.String) -> pt.Expr:
30+
return output.set(pt.Concat(pt.Bytes("Hello, "), name.get()))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{% raw -%}
2+
import logging
3+
4+
import algokit_utils
5+
from algosdk.v2client.algod import AlgodClient
6+
from algosdk.v2client.indexer import IndexerClient
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
# define deployment behaviour based on supplied app spec
12+
def deploy(
13+
algod_client: AlgodClient,
14+
indexer_client: IndexerClient,
15+
app_spec: algokit_utils.ApplicationSpecification,
16+
deployer: algokit_utils.Account,
17+
) -> None:
18+
from smart_contracts.artifacts.{{ contract_name }}.client import (
19+
{{ contract_name.split('_')|map('capitalize')|join }}Client,
20+
)
21+
22+
app_client = {{ contract_name.split('_')|map('capitalize')|join }}Client(
23+
algod_client,
24+
creator=deployer,
25+
indexer_client=indexer_client,
26+
)
27+
28+
29+
{%- if preset_name == 'starter' %}
30+
app_client.deploy(
31+
on_schema_break=algokit_utils.OnSchemaBreak.AppendApp,
32+
on_update=algokit_utils.OnUpdate.AppendApp,
33+
)
34+
{%- elif preset_name == 'production' %}
35+
is_mainnet = algokit_utils.is_mainnet(algod_client)
36+
app_client.deploy(
37+
on_schema_break=(
38+
algokit_utils.OnSchemaBreak.AppendApp
39+
if is_mainnet
40+
else algokit_utils.OnSchemaBreak.ReplaceApp
41+
),
42+
on_update=algokit_utils.OnUpdate.AppendApp
43+
if is_mainnet
44+
else algokit_utils.OnUpdate.UpdateApp,
45+
allow_delete=not is_mainnet,
46+
allow_update=not is_mainnet,
47+
)
48+
{%- endif %}
49+
50+
name = "world"
51+
response = app_client.hello(name=name)
52+
logger.info(
53+
f"Called hello on {app_spec.contract.name} ({app_client.app_id}) "
54+
f"with name={name}, received: {response.return_value}"
55+
)
56+
{% endraw -%}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{% raw -%}
2+
import * as algokit from '@algorandfoundation/algokit-utils'
3+
import { {{ contract_name.split('_')|map('capitalize')|join }}Client } from '../artifacts/{{ contract_name }}/client'
4+
5+
// Below is a showcase of various deployment options you can use in TypeScript Client
6+
export async function deploy() {
7+
console.log('=== Deploying {{ contract_name.split('_')|map('capitalize')|join }} ===')
8+
9+
const algod = algokit.getAlgoClient()
10+
const indexer = algokit.getAlgoIndexerClient()
11+
const deployer = await algokit.getAccount(
12+
{ config: algokit.getAccountConfigFromEnvironment('DEPLOYER'), fundWith: algokit.algos(3) },
13+
algod,
14+
)
15+
await algokit.ensureFunded(
16+
{
17+
accountToFund: deployer,
18+
minSpendingBalance: algokit.algos(2),
19+
minFundingIncrement: algokit.algos(2),
20+
},
21+
algod,
22+
)
23+
const isMainNet = await algokit.isMainNet(algod)
24+
const appClient = new {{ contract_name.split('_')|map('capitalize')|join }}Client(
25+
{
26+
resolveBy: 'creatorAndName',
27+
findExistingUsing: indexer,
28+
sender: deployer,
29+
creatorAddress: deployer.addr,
30+
},
31+
algod,
32+
)
33+
34+
{%- if preset_name == 'starter' %}
35+
const app = await appClient.deploy({
36+
onSchemaBreak: 'append',
37+
onUpdate: 'append',
38+
})
39+
{% elif preset_name == 'production' %}
40+
const app = await appClient.deploy({
41+
allowDelete: !isMainNet,
42+
allowUpdate: !isMainNet,
43+
onSchemaBreak: isMainNet ? 'append' : 'replace',
44+
onUpdate: isMainNet ? 'append' : 'update',
45+
})
46+
{% endif %}
47+
48+
// If app was just created fund the app account
49+
if (['create', 'replace'].includes(app.operationPerformed)) {
50+
algokit.transferAlgos(
51+
{
52+
amount: algokit.algos(1),
53+
from: deployer,
54+
to: app.appAddress,
55+
},
56+
algod,
57+
)
58+
}
59+
60+
const method = 'hello'
61+
const response = await appClient.hello({ name: 'world' })
62+
console.log(`Called ${method} on ${app.name} (${app.appId}) with name = world, received: ${response.return}`)
63+
}
64+
{% endraw -%}

template_content/smart_contracts/README.md.jinja

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
By the default the template creates a single `HelloWorld` contract under {{ contract_name }} folder in the `smart_contracts` directory. To add a new contract:
44

5-
1. Create a new folder under `smart_contracts` directory and add define your new contract in `contract.py` file.
6-
2. Each contract has potentially has different creation parameters and deployment steps. Hence, you need to define your deployment logic in {% if deployment_language == 'python' %}`deploy_config.py`{% elif deployment_language == 'typescript' %}`deploy-config.ts`{% endif %}file.
7-
3. Reference your contract in `config.py` file. This will tell instruct the helper scripts on which exact contracts require building and generation of typed clients.
5+
1. From the root of the repository execute `algokit generate smart-contract`. This will create a new starter smart contract and deployment configuration file under `{your_contract_name}` subfolder under `smart_contracts` directory.
6+
2. Each contract potentially has different creation parameters and deployment steps. Hence, you need to define your deployment logic in {% if deployment_language == 'python' %}`deploy_config.py`{% elif deployment_language == 'typescript' %}`deploy-config.ts`{% endif %}file.
7+
3. `config.py` file will automatically build all contracts under `smart_contracts` directory. If you want to build specific contracts manually, modify the default code provided by the template in `config.py` file.
88
{%- if deployment_language == 'typescript' %}
9-
4. Since you are generating a TypeScript client, you also need to reference your contract in `index.ts` file. This will instruct the TypeScript specific helper scripts on how exactly to deploy your contract.
9+
4. Since you are generating a TypeScript client, you also need to reference your contract deployment logic in `index.ts` file. However, similar to config.py, by default, `index.ts` will auto import all TypeScript deployment files under `smart_contracts` directory. If you want to manually import specific contracts, modify the default code provided by the template in `index.ts` file.
1010
{%- endif %}
1111

12-
> Please note, above is just a suggested convention tailored for the base configuration and structure of this template. You are free to modify the structure and naming conventions as you see fit.
12+
> Please note, above is just a suggested convention tailored for the base configuration and structure of this template. Default code supplied by the template in `config.py` and `index.ts` (if using ts clients) files are tailored for the suggested convention. You are free to modify the structure and naming conventions as you see fit.
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import dataclasses
2+
import importlib
23
from collections.abc import Callable
4+
from pathlib import Path
35

46
from algokit_utils import Account, ApplicationSpecification
57
from algosdk.v2client.algod import AlgodClient
68
from algosdk.v2client.indexer import IndexerClient
79
from beaker import Application
810

9-
from smart_contracts.{{ contract_name }}.contract import app as {{ contract_name }}_app
10-
{% if deployment_language == 'python' -%}
11-
from smart_contracts.{{ contract_name }}.deploy_config import deploy as {{ contract_name }}_deploy
12-
{% endif %}
1311

1412
@dataclasses.dataclass
1513
class SmartContract:
@@ -19,10 +17,48 @@ class SmartContract:
1917
] | None = None
2018

2119

22-
{% if deployment_language == 'python' -%}
20+
def import_contract(folder: Path) -> Application:
21+
"""Imports the contract from a folder if it exists."""
22+
try:
23+
contract_module = importlib.import_module(
24+
f"{folder.parent.name}.{folder.name}.contract"
25+
)
26+
return contract_module.app
27+
except ImportError as e:
28+
raise Exception(f"Contract not found in {folder}") from e
29+
30+
31+
def import_deploy_if_exists(
32+
folder: Path,
33+
) -> (
34+
Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None]
35+
| None
36+
):
37+
"""Imports the deploy function from a folder if it exists."""
38+
try:
39+
deploy_module = importlib.import_module(
40+
f"{folder.parent.name}.{folder.name}.deploy_config"
41+
)
42+
return deploy_module.deploy
43+
except ImportError:
44+
return None
45+
46+
47+
def has_contract_file(directory: Path) -> bool:
48+
"""Checks whether the directory contains contract.py file."""
49+
return (directory / "contract.py").exists()
50+
51+
2352
# define contracts to build and/or deploy
24-
contracts = [SmartContract(app={{ contract_name }}_app, deploy={{ contract_name }}_deploy)]
25-
{% elif deployment_language == 'typescript' -%}
26-
# define contracts to build
27-
contracts = [SmartContract(app={{ contract_name }}_app)]
28-
{% endif -%}
53+
base_dir = Path("smart_contracts")
54+
contracts = [
55+
SmartContract(app=import_contract(folder), deploy=import_deploy_if_exists(folder))
56+
for folder in base_dir.iterdir()
57+
if folder.is_dir() and has_contract_file(folder)
58+
]
59+
60+
## Comment the above and uncomment the below and define contracts manually if you want to build and specify them
61+
## manually otherwise the above code will always include all contracts under contract.py file for any subdirectory
62+
## in the smart_contracts directory. Optionally it will also grab the deploy function from deploy_config.py if it exists.
63+
64+
# contracts = []

template_content/smart_contracts/{% if deployment_language == 'typescript' %}index.ts{% endif %}.jinja

+28-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
13
import { consoleLogger } from '@algorandfoundation/algokit-utils/types/logging'
24
import * as algokit from '@algorandfoundation/algokit-utils'
3-
import { deploy as {{ contract_name.split('_')|map('capitalize')|join }}Deployer } from './{{ contract_name }}/deploy-config'
4-
5-
const contractDeployers = [{{ contract_name.split('_')|map('capitalize')|join }}Deployer]
65

76
algokit.Config.configure({
87
logger: consoleLogger,
98
})
10-
;(async () => {
9+
10+
// base directory
11+
const baseDir = path.resolve(__dirname)
12+
13+
// function to validate and dynamically import a module
14+
async function importDeployerIfExists(dir: string) {
15+
const deployerPath = path.resolve(dir, 'deploy-config')
16+
if (fs.existsSync(deployerPath + '.ts') || fs.existsSync(deployerPath + '.js')) {
17+
const deployer = await import(deployerPath)
18+
return deployer.deploy
19+
}
20+
}
21+
22+
// get a list of all deployers from the subdirectories
23+
async function getDeployers() {
24+
const directories = fs.readdirSync(baseDir, { withFileTypes: true })
25+
.filter(dirent => dirent.isDirectory())
26+
.map(dirent => path.resolve(baseDir, dirent.name))
27+
28+
return Promise.all(directories.map(importDeployerIfExists))
29+
}
30+
31+
// execute all the deployers
32+
(async () => {
33+
const contractDeployers = (await getDeployers()).filter(Boolean)
34+
1135
for (const deployer of contractDeployers) {
1236
try {
1337
await deployer()

0 commit comments

Comments
 (0)