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