Smart-contract lifecycle
SmartContractCallFactory / SmartContractController cover calling an already-deployed contract. For the contract's lifecycle itself -- deploy, upgrade, change owner, claim developer rewards -- use SmartContractTransactionsFactory.
These four operations are ABI-free: they don't touch the endpoint list. They're plain data-field patterns the chain recognises on its own.
Setup
import 'package:abidock_mvx/abidock_mvx.dart';
final factory = SmartContractTransactionsFactory(
SmartContractTransactionsConfig(chainId: const ChainId.devnet()),
);
SmartContractTransactionsConfig carries the usual gas floors (minGasLimit, gasLimitPerByte, defaultGasPrice) plus per-operation caps for claimDeveloperRewards and changeOwnerAddress. Defaults match mainnet; override per-field when the chain tunes them.
Deploy
final Uint8List bytecode = await File('my-contract.wasm').readAsBytes();
final Transaction deployTx = factory.createTransactionForDeploy(
sender: deployer.address,
bytecode: bytecode,
gasLimit: const GasLimit(60_000_000),
arguments: <Uint8List>[
Uint8List.fromList('hello'.codeUnits), // constructor arg
],
// codeMetadata defaults to [0x05, 0x06] -- upgradeable + readable + payable + payableBySC.
// Pass a `CodeMetadataValue.toBytes()` result here to customise.
);
The receiver is set to Address.zero() (the convention for deploys). vmType defaults to '0500' (WASM). Data-field shape:
<codeHex>@<vmTypeHex>@<codeMetadataHex>[@<argHex>...]
After signing and broadcasting, compute the contract address locally with AddressComputer.computeContractAddress(deployer, nonce) -- the same nonce used on the tx.
Upgrade
final Transaction upgradeTx = factory.createTransactionForUpgrade(
sender: owner.address,
contract: existingContractAddress,
bytecode: newBytecode,
gasLimit: const GasLimit(60_000_000),
arguments: <Uint8List>[/* args to the new init function, if any */],
);
Receiver is the contract itself. Data-field shape: upgradeContract@<codeHex>@<metadataHex>[@<argHex>...]. Only the current owner can upgrade; the chain rejects any other sender.
Change owner
final Transaction tx = factory.createTransactionForChangeOwnerAddress(
sender: currentOwner.address,
contract: contractAddress,
newOwner: newOwner.address,
);
Data-field: ChangeOwnerAddress@<newOwnerHex>. Gas floor is computed automatically from gasLimitChangeOwnerAddress (6M default) plus the move-balance portion; no explicit gasLimit parameter.
Claim developer rewards
final Transaction tx = factory.createTransactionForClaimDeveloperRewards(
sender: owner.address,
contract: contractAddress,
);
Data-field: ClaimDeveloperRewards (no args). Must be sent by the current contract owner.
Signing and broadcast
Each of the four transactions is a plain Transaction -- sign with UserSigner.sign(tx.serializeForSigning()) (or via Account.signTransaction(tx) if you have an Account wrapper), then broadcast through the network provider.
final sigBytes = await account.signTransaction(deployTx);
final signedTx = deployTx.copyWith(newSignature: Signature.fromBytes(sigBytes));
final hash = await provider.sendTransaction(signedTx);
print('deploy tx: $hash');
final contractAddress = AddressComputer.computeContractAddress(
account.address,
deployTx.nonce,
);
print('deployed to: ${contractAddress.bech32}');
Decoding lifecycle transactions
TransactionDecoder understands all four patterns as of 1.0.2. Pattern-match on the sealed subclasses:
switch (const TransactionDecoder().decode(tx)) {
case ContractDeploy(:final bytecode, :final arguments):
...
case ContractUpgrade(:final bytecode, :final codeMetadata):
...
case ContractChangeOwner(:final newOwner):
...
case ClaimDeveloperRewards():
...
default:
// not a lifecycle tx
}
See Decoding Transactions for the full variant list.