App Deployer
Description
Section titled “Description”This example demonstrates the AppDeployer functionality for idempotent application deployment with create, update, and replace strategies:
- algorand.appDeployer.deploy() for initial deployment
- Deploy parameters: name, version, approvalProgram, clearProgram, schema, onUpdate, onSchemaBreak
- Idempotency: calling deploy() again with same version does nothing
- onUpdate: ‘update’ to update existing app when version changes
- onUpdate: ‘replace’ to delete and recreate app when version changes
- onUpdate: ‘fail’ to fail if app already exists with different code
- onSchemaBreak: ‘replace’ when schema changes require new app
- Deployment metadata stored in app global state
- App name used for idempotent lookups
Prerequisites
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example algorand_client/13-app-deployer.ts/** * Example: App Deployer * * This example demonstrates the AppDeployer functionality for idempotent * application deployment with create, update, and replace strategies: * - algorand.appDeployer.deploy() for initial deployment * - Deploy parameters: name, version, approvalProgram, clearProgram, schema, onUpdate, onSchemaBreak * - Idempotency: calling deploy() again with same version does nothing * - onUpdate: 'update' to update existing app when version changes * - onUpdate: 'replace' to delete and recreate app when version changes * - onUpdate: 'fail' to fail if app already exists with different code * - onSchemaBreak: 'replace' when schema changes require new app * - Deployment metadata stored in app global state * - App name used for idempotent lookups * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */
import { AlgorandClient, algo } from '@algorandfoundation/algokit-utils'import { loadTealSource, printError, printHeader, printInfo, printStep, printSuccess, shortenAddress } from '../shared/utils.js'
// ============================================================================// TEAL Programs - Versioned Application (loaded from shared artifacts)// ============================================================================
/** * Generate a versioned approval program that supports updates and deletes. * Uses TMPL_UPDATABLE and TMPL_DELETABLE for deploy-time control. * The version parameter changes the bytecode to simulate code updates. */function getVersionedApprovalProgram(version: number): string { return loadTealSource('teal-template-versioned.teal').replace(/TMPL_VERSION/g, String(version))}
// Clear state program (always approves)const CLEAR_STATE_PROGRAM = loadTealSource('clear-state-approve.teal')
async function main() { printHeader('App Deployer Example')
// Initialize client and verify LocalNet is running const algorand = AlgorandClient.defaultLocalNet()
try { await algorand.client.algod.status() printSuccess('Connected to LocalNet') } catch (error) { printError(`Failed to connect to LocalNet: ${error instanceof Error ? error.message : String(error)}`) printInfo('Make sure LocalNet is running (e.g., algokit localnet start)') return }
// Step 1: Create and fund test accounts printStep(1, 'Create and fund test accounts') printInfo('Creating account for app deployment demonstrations')
const deployer = algorand.account.random()
printInfo(`\nCreated account:`) printInfo(` Deployer: ${shortenAddress(deployer.addr.toString())}`)
// Fund account generously for multiple deployments await algorand.account.ensureFundedFromEnvironment(deployer.addr, algo(50))
printSuccess('Created and funded test account')
// Step 2: Initial deployment with appDeployer.deploy() printStep(2, 'Initial deployment with algorand.appDeployer.deploy()') printInfo('Deploying a versioned application for the first time')
const appName = 'MyVersionedApp'
const result1 = await algorand.appDeployer.deploy({ metadata: { name: appName, version: '1.0.0', updatable: true, // Allow updates via TMPL_UPDATABLE deletable: true, // Allow deletion via TMPL_DELETABLE }, createParams: { sender: deployer.addr, approvalProgram: getVersionedApprovalProgram(1), clearStateProgram: CLEAR_STATE_PROGRAM, schema: { globalInts: 2, // version, counter globalByteSlices: 0, localInts: 0, localByteSlices: 0, }, }, updateParams: { sender: deployer.addr, }, deleteParams: { sender: deployer.addr, }, })
printInfo(`\nDeployment result:`) printInfo(` Operation performed: ${result1.operationPerformed}`) printInfo(` App ID: ${result1.appId}`) printInfo(` App Address: ${shortenAddress(result1.appAddress.toString())}`) printInfo(` App Name: ${result1.name}`) printInfo(` Version: ${result1.version}`) printInfo(` Updatable: ${result1.updatable}`) printInfo(` Deletable: ${result1.deletable}`) if ('transaction' in result1) { printInfo(` Transaction ID: ${result1.txIds[0]}`) }
printSuccess('Initial deployment completed (operation: create)')
// Step 3: Demonstrate idempotency - same version does nothing printStep(3, 'Demonstrate idempotency - deploy same version again') printInfo('Calling deploy() again with the same version should do nothing')
const result2 = await algorand.appDeployer.deploy({ metadata: { name: appName, version: '1.0.0', // Same version updatable: true, deletable: true, }, createParams: { sender: deployer.addr, approvalProgram: getVersionedApprovalProgram(1), // Same code clearStateProgram: CLEAR_STATE_PROGRAM, schema: { globalInts: 2, globalByteSlices: 0, localInts: 0, localByteSlices: 0, }, }, updateParams: { sender: deployer.addr, }, deleteParams: { sender: deployer.addr, }, })
printInfo(`\nIdempotent deployment result:`) printInfo(` Operation performed: ${result2.operationPerformed}`) printInfo(` App ID: ${result2.appId} (same as before)`) printInfo(` Version: ${result2.version}`) if (result2.operationPerformed === 'nothing') { printInfo(` Note: No transaction was sent - app is unchanged`) }
printSuccess('Idempotency verified - no action taken for same version')
// Step 4: Demonstrate onUpdate: 'update' printStep(4, "Demonstrate onUpdate: 'update' - update existing app") printInfo('Deploying version 2.0.0 with onUpdate: "update" to update the existing app')
const result3 = await algorand.appDeployer.deploy({ metadata: { name: appName, version: '2.0.0', // New version updatable: true, deletable: true, }, createParams: { sender: deployer.addr, approvalProgram: getVersionedApprovalProgram(2), // Updated code clearStateProgram: CLEAR_STATE_PROGRAM, schema: { globalInts: 2, globalByteSlices: 0, localInts: 0, localByteSlices: 0, }, }, updateParams: { sender: deployer.addr, }, deleteParams: { sender: deployer.addr, }, onUpdate: 'update', // Update the existing app })
printInfo(`\nUpdate deployment result:`) printInfo(` Operation performed: ${result3.operationPerformed}`) printInfo(` App ID: ${result3.appId} (same app, updated in place)`) printInfo(` Version: ${result3.version}`) printInfo(` Created round: ${result3.createdRound}`) printInfo(` Updated round: ${result3.updatedRound}`) if ('transaction' in result3) { printInfo(` Transaction ID: ${result3.txIds[0]}`) }
// Verify the global state was preserved but version updated const globalState = await algorand.app.getGlobalState(result3.appId) printInfo(`\nGlobal state after update:`) printInfo(` version: ${globalState['version']?.value ?? 'N/A'} (from TEAL)`) printInfo(` counter: ${globalState['counter']?.value ?? 'N/A'} (preserved)`)
printSuccess('App updated in place with new code')
// Step 5: Demonstrate onUpdate: 'fail' printStep(5, "Demonstrate onUpdate: 'fail' - fails if update detected") printInfo('Trying to deploy version 3.0.0 with onUpdate: "fail" should throw an error')
try { await algorand.appDeployer.deploy({ metadata: { name: appName, version: '3.0.0', // New version updatable: true, deletable: true, }, createParams: { sender: deployer.addr, approvalProgram: getVersionedApprovalProgram(3), clearStateProgram: CLEAR_STATE_PROGRAM, schema: { globalInts: 2, globalByteSlices: 0, localInts: 0, localByteSlices: 0, }, }, updateParams: { sender: deployer.addr, }, deleteParams: { sender: deployer.addr, }, onUpdate: 'fail', // Fail if update detected }) printError('Expected an error but deployment succeeded') } catch (error) { printInfo(`\nExpected error caught:`) printInfo(` ${error instanceof Error ? error.message : String(error)}`) printSuccess('onUpdate: "fail" correctly prevents updates') }
// Step 6: Demonstrate onUpdate: 'replace' printStep(6, "Demonstrate onUpdate: 'replace' - delete and recreate app") printInfo('Deploying version 3.0.0 with onUpdate: "replace" deletes old app and creates new one')
const oldAppId = result3.appId
const result4 = await algorand.appDeployer.deploy({ metadata: { name: appName, version: '3.0.0', updatable: true, deletable: true, }, createParams: { sender: deployer.addr, approvalProgram: getVersionedApprovalProgram(3), clearStateProgram: CLEAR_STATE_PROGRAM, schema: { globalInts: 2, globalByteSlices: 0, localInts: 0, localByteSlices: 0, }, }, updateParams: { sender: deployer.addr, }, deleteParams: { sender: deployer.addr, }, onUpdate: 'replace', // Delete old and create new })
printInfo(`\nReplace deployment result:`) printInfo(` Operation performed: ${result4.operationPerformed}`) printInfo(` Old App ID: ${oldAppId} (deleted)`) printInfo(` New App ID: ${result4.appId}`) printInfo(` App Address: ${shortenAddress(result4.appAddress.toString())}`) printInfo(` Version: ${result4.version}`) if (result4.operationPerformed === 'replace' && 'deleteResult' in result4) { printInfo(` Delete transaction confirmed: round ${result4.deleteResult.confirmation.confirmedRound}`) }
printSuccess('Old app deleted and new app created')
// Step 7: Demonstrate onSchemaBreak: 'replace' printStep(7, "Demonstrate onSchemaBreak: 'replace' - handle schema changes") printInfo('Deploying version 4.0.0 with increased schema (more global ints)') printInfo('Schema changes cannot be done via update, so replace is required')
const result5 = await algorand.appDeployer.deploy({ metadata: { name: appName, version: '4.0.0', updatable: true, deletable: true, }, createParams: { sender: deployer.addr, approvalProgram: getVersionedApprovalProgram(4), clearStateProgram: CLEAR_STATE_PROGRAM, schema: { globalInts: 3, // Schema break: increased from 2 to 3 globalByteSlices: 1, // Schema break: increased from 0 to 1 localInts: 0, localByteSlices: 0, }, }, updateParams: { sender: deployer.addr, }, deleteParams: { sender: deployer.addr, }, onUpdate: 'update', // Would normally try to update onSchemaBreak: 'replace', // But schema change forces replace })
printInfo(`\nSchema break deployment result:`) printInfo(` Operation performed: ${result5.operationPerformed}`) printInfo(` Previous App ID: ${result4.appId}`) printInfo(` New App ID: ${result5.appId}`) printInfo(` Version: ${result5.version}`)
// Verify new schema const appInfo = await algorand.app.getById(result5.appId) printInfo(`\nNew app schema:`) printInfo(` globalInts: ${appInfo.globalInts}`) printInfo(` globalByteSlices: ${appInfo.globalByteSlices}`)
printSuccess('Schema break handled with replace strategy')
// Step 8: Show deployment metadata in global state printStep(8, 'Show deployment metadata lookup by name') printInfo('The appDeployer uses app name for idempotent lookups across deployments')
// Look up the app by creator const creatorApps = await algorand.appDeployer.getCreatorAppsByName(deployer.addr)
printInfo(`\nApps deployed by ${shortenAddress(deployer.addr.toString())}:`) for (const [name, appMeta] of Object.entries(creatorApps.apps)) { printInfo(`\n App Name: "${name}"`) printInfo(` App ID: ${appMeta.appId}`) printInfo(` Version: ${appMeta.version}`) printInfo(` Updatable: ${appMeta.updatable}`) printInfo(` Deletable: ${appMeta.deletable}`) printInfo(` Created Round: ${appMeta.createdRound}`) printInfo(` Updated Round: ${appMeta.updatedRound}`) printInfo(` Deleted: ${appMeta.deleted}`) printInfo(` Created Metadata:`) printInfo(` Name: ${appMeta.createdMetadata.name}`) printInfo(` Version: ${appMeta.createdMetadata.version}`) }
printSuccess('Deployment metadata retrieved')
// Step 9: Demonstrate how name enables idempotency printStep(9, 'Demonstrate how app name enables idempotent deployments') printInfo('Deploy a second app with a different name to show name-based lookup')
const result6 = await algorand.appDeployer.deploy({ metadata: { name: 'AnotherApp', // Different name version: '1.0.0', updatable: false, deletable: false, }, createParams: { sender: deployer.addr, approvalProgram: getVersionedApprovalProgram(100), clearStateProgram: CLEAR_STATE_PROGRAM, schema: { globalInts: 2, // version, counter globalByteSlices: 0, localInts: 0, localByteSlices: 0, }, }, updateParams: { sender: deployer.addr, }, deleteParams: { sender: deployer.addr, }, })
printInfo(`\nSecond app deployment:`) printInfo(` Name: AnotherApp`) printInfo(` App ID: ${result6.appId}`) printInfo(` Operation: ${result6.operationPerformed}`)
// Now list all apps again const allApps = await algorand.appDeployer.getCreatorAppsByName(deployer.addr) printInfo(`\nAll apps by creator (${Object.keys(allApps.apps).length} apps):`) for (const name of Object.keys(allApps.apps)) { printInfo(` - "${name}" (App ID: ${allApps.apps[name].appId})`) }
printSuccess('Multiple apps tracked by name')
// Step 10: Summary printStep(10, 'Summary - App Deployer API') printInfo('The AppDeployer provides idempotent application deployment:') printInfo('') printInfo('algorand.appDeployer.deploy(params):') printInfo(' - Deploys applications with idempotent behavior based on app name') printInfo(' - Returns: AppDeployResult with operationPerformed discriminator') printInfo('') printInfo('Key parameters:') printInfo(' metadata: { name, version, updatable, deletable }') printInfo(' - name: Unique identifier for idempotent lookups') printInfo(' - version: Semantic version string') printInfo(' - updatable/deletable: Deploy-time controls (TMPL_UPDATABLE/TMPL_DELETABLE)') printInfo('') printInfo(' createParams: { sender, approvalProgram, clearStateProgram, schema }') printInfo(' updateParams: { sender } - Used for update operations') printInfo(' deleteParams: { sender } - Used for replace operations') printInfo('') printInfo(' onUpdate: Controls behavior when code changes:') printInfo(" 'fail' - Throw error (default)") printInfo(" 'update' - Update app in place (preserves app ID)") printInfo(" 'replace' - Delete old app, create new one") printInfo(" 'append' - Create new app, leave old one") printInfo('') printInfo(' onSchemaBreak: Controls behavior when schema changes:') printInfo(" 'fail' - Throw error (default)") printInfo(" 'replace' - Delete old app, create new one") printInfo(" 'append' - Create new app, leave old one") printInfo('') printInfo('operationPerformed values:') printInfo(" 'create' - New app was created") printInfo(" 'update' - Existing app was updated in place") printInfo(" 'replace' - Old app deleted, new app created") printInfo(" 'nothing' - No changes (idempotent)") printInfo('') printInfo('algorand.appDeployer.getCreatorAppsByName(creator):') printInfo(' - Lists all apps deployed by a creator with their metadata') printInfo(' - Used internally for idempotent lookup by name')
printSuccess('App Deployer example completed!')}
main().catch((error) => { printError(`Unhandled error: ${error instanceof Error ? error.message : String(error)}`) process.exit(1)})Other examples in Algorand Client
Section titled “Other examples in Algorand Client”- Client Instantiation
- AlgoAmount Utility
- Signer Configuration
- Suggested Params Configuration
- Account Manager
- Send Payment
- Send Asset Operations
- Send Application Operations
- Create Transaction (Unsigned Transactions)
- Transaction Composer (Atomic Transaction Groups)
- Asset Manager
- App Manager
- App Deployer
- Client Manager
- Error Transformers
- Transaction Leases