DAOrayaki DAO Research Bonus Pool:
Funding address: DAOrayaki.eth
Voting progress: DAO Reviewer 3/0 passed
Research Type: Aptos,Layer1
Creator: FF@DoraFactory
This article mainly explains the operation of aptos cli and aptos sdk
Clone Aptos-core Repo
# Clone the Aptos repo.
git clone
# cd into aptos-core directory.
cd aptos-core
# Run the scripts/dev_setup.sh Bash script as shown below. This will prepare your developer environment.
./scripts/dev_setup.sh
# Update your current shell environment.
source ~/.cargo/env
# Skip this step if you are not installing an Aptos node.
git checkout --track origin/devnet
Start the Aptos local chain
Using CLI to Run a Local Testnet | Aptos Docs[1]
Start the local chain
ps: The local chain and data started by this method will be saved in the current folder where this command is started, and exist in the .aptos/ file
aptos node run-local-testnet --with-faucet
Started successfully:
Completed generating configuration:
Log file: "/Users/greg/.aptos/testnet/validator.log"
Test dir: "/Users/greg/.aptos/testnet"
Aptos root key path: "/Users/greg/.aptos/testnet/mint.key"
Waypoint: 0:74c9d14285ec19e6bd15fbe851007ea8b66efbd772f613c191aa78721cadac25
ChainId: TESTING
REST API endpoint: 0.0.0.0:8080
FullNode network: /ip4/0.0.0.0/tcp/6181
Aptos is running, press ctrl-c to exit
Faucet is running. Faucet endpoint: 0.0.0.0:8081
After successful startup, it will prompt the address of rest api and faucet api. Later, these two pieces of information need to be configured in the aptos cli environment.
Configure the aptos cli environment
In order to access and call the local test chain through the command line, we need to configure config for aptos cli according to the above deployment information.
PROFILE=local
aptos init --profile $PROFILE --rest-url --faucet-url
During execution, we will get the following output. We can choose to enter a secret key, or it can be randomly generated by default.
Configuring for profile local
Using command line argument for rest URL
Using command line argument for faucet URL
Enter your private key as a hex literal (0x...) [Current: None | No input: Generate new key (or keep one if present)]
After confirmation, an account will be created and funded with the default number of tokens.
No key given, generating key...
Account 7100C5295ED4F9F39DCC28D309654E291845984518307D3E2FE00AEA5F8CACC1 doesn't exist, creating it and funding it with 10000 coins
Aptos is now set up for account 7100C5295ED4F9F39DCC28D309654E291845984518307D3E2FE00AEA5F8CACC1! Run `aptos help` for more information about commands
{
"Result": "Success"
}
From now on, we can run them on the local testnet by adding --profile local to the command.
ps: The --profile here is like kube-config in k8s, which can set different profile environments and control different networks.
The profile configuration will set the executor address, node-rest-api, and faucet-api information.
# List all accounts controlled by cli
aptos account list
# Fund the account:
aptos account fund --profile $PROFILE --account $PROFILE
# Create a new resource account
aptos account create-resource-account --profile $PROFILE --seed 1
# Compile the move contract
aptos move compile --package-dir hello_blockchain
# Deploy the contract
aptos move publish --package-dir hello_blockchain --named-addresses basecoin= --profile local
# call the contract
aptos move run --function-id :::: --profile local
# List the modules/resources information of the specified account
aptos account list --query modules --account 0xa1285adb4b8abedf5faf7a46d260c5844f1f64d59dd9b8869db1543cf5bbadf4 --profile local
aptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf --profile local
# Contract upgrade
aptos move publish --upgrade-policy
`arbitrary`, `compatible`,`immutable` corresponds to 0, 1, 2
0 does not do any checks, force replacement code,
1 Do a compatibility check (the same public function cannot change the memory layout of the existing Resource)
2 Prohibition of upgrades
Every time it is published, it will compare the policy on the chain with the policy of this publication (the default is 1),
Contract upgrades are allowed only when the policy this time is smaller than the policy on the chain
Deploy a simple Move contract
module MyCounterAddr::MyCounter {
use std::signer;
struct Counter has key, store {
value:u64,
}
public fun init(account: &signer){
move_to(account, Counter{value:0});
}
public fun incr(account: &signer) acquires Counter {
let counter = borrow_global_mut(signer::address_of(account));
counter.value = counter.value + 1;
}
public entry fun init_counter(account: signer){
Self::init(&account)
}
public entry fun incr_counter(account: signer) acquires Counter {
Self::incr(&account)
}
}
MyCounter source code analysis
A module is a packaged set of functions and structures published under a specific address. When using script, it needs to run with the published module or standard library, and the standard library itself is a group of modules released under the 0x1 address.
module MyCounterAddr::MyCounter{ } is under the MyCounterAddr address (corresponding to MyCounterAddr ="0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf") to create a module.
use std::signer is to use the signer module under the standard library. Signer is a native non-copyable type similar to Resource, which contains the address of the transaction sender. One of the reasons for introducing the signer type was to make it clear which functions require sender permissions and which do not. Therefore, a function cannot trick a user into gaining unauthorized access to its Resource. For details, please refer to the source code [2].
module std::signer {
// Borrows the address of the signer
// Conceptually, you can think of the `signer` as being a struct wrapper arround an
// address
// ```
// struct signer has drop { addr: address }
// ```
// `borrow_address` borrows this inner field
native public fun borrow_address(s: &signer): &address;
// Copies the address of the signer
public fun address_of(s: &signer): address {
*borrow_address(s)
}
/// Return true only if `s` is a transaction signer. This is a spec function only available in spec.
spec native fun is_txn_signer(s: signer): bool;
/// Return true only if `a` is a transaction signer address. This is a spec function only available in spec.
spec native fun is_txn_signer_addr(a: address): bool;
}
Struct & Abilities
struct Counter has key, store {
value:u64,
}
Use struct to define a structure called Counter, and at the same time, it is modified by two kinds of limiters, key and store.
Move's type system is flexible, and each type can define four abilities.
They define whether values of a type can be copied, discarded and stored.
The four abilities qualifiers are: Copy, Drop, Store and Key.
Their functions are:
Copy - Value can be copied.
Drop - Values can be dropped at the end of the scope.
Key - The value can be accessed as a key by "global storage operations".
Store - Values can be stored to global state.
Modified with key and store here, it means that it cannot be copied, discarded or reused, but it can be safely stored and transferred.
Syntax for Abilities
The abilities of primitive types and built-in types are pre-defined and immutable: values of integers, vector, addresses and boolean types inherently have copy, drop and store abilities.
However, the ability of the structure can be added according to the following syntax:
struct NAME has ABILITY [, ABILITY] { [FIELDS] }
A simple library example:
module Library {
// each ability has matching keyword
// multiple abilities are listed with comma
struct Book has store, copy, drop {
year: u64
}
// single ability is also possible
struct Storage has key {
books: vector
}
// this one has no abilities
struct Empty {}
}
What is Resource
The concept of Resource is described in detail in the Move white paper. Initially, it was implemented as a structure type called resource. Since the introduction of ability, it has been implemented as a structure with two abilities of Key and Store. Resource can safely represent digital assets, it cannot be copied, nor can it be discarded or reused, but it can be safely stored and transferred.
Definition of Resource
Resource is a struct limited with key and store ability:
module M {
struct T has key, store {
field: u8
}
}Resource limit
In code, the Resource type has several major limitations:
Resources are stored under accounts. Therefore, it only exists when an account is assigned and can only be accessed through that account.
An account can only hold one resource of a certain type at a time.
Resource cannot be copied; corresponding to it is a special kind: resource, which is different from copyable, which has been introduced in the chapter on generics. (This can be abstracted to Rust's ownership)
A Resource must be used, which means that a newly created Resource must be moved under an account, and a Resource moved from an account must be deconstructed or stored under another account.
The case just now
struct Counter has key, store {
value:u64,
}
So here is a difference from solidity, if a new asset needs to be issued on eth, such as usdc. Then this asset is recorded in a map in the contract. But move is different, the asset is stored under the user's address as a resource.
define function
public fun init(account: &signer){
move_to(account, Counter{value:0});
}
public fun incr(account: &signer) acquires Counter {
let counter = borrow_global_mut(signer::address_of(account));
counter.value = counter.value + 1;
}
public entry fun init_counter(account: signer){
Self::init(&account)
}
public entry fun incr_counter(account: signer) acquires Counter {
Self::incr(&account)
}
The definition format is:
public fun function name (parameter: parameter type) { }
move functions are private by default and can only be accessed within the module in which they are defined. The keyword public will change the default visibility of a function and make it public, ie accessible from the outside.
The parameter of the init method is a &signer, which means that this method can only be called after an account is legally signed. move_to is a primitive of move, which is used to publish and add Counter resources to the address of the signer. In Move's account model, code and data are stored under an account address.
The following is a list of commonly used primitives
move_to< T >(&signer,T): Publish and add a Resource of type T to the signer's address.
move_from< T >(addr: address): T - Removes a Resource of type T from address and returns this resource.
borrow_global< T >(addr: address): &T - Returns an immutable reference to the Resource of type T at the address.
borrow_global_mut< T >(addr: address): &mut T - Returns a mutable reference to the Resource of type T at address.
exists< T >(address): bool: Determine whether there is a Resource of type T under the address.
The parameter of the incr method is also a &signer, which means that the method must be legally signed by an account before it can be called.
The keyword acquires, placed after the return value of the function, is used to explicitly define all the resources acquired by this function.
Signer::address_of(account) Get the address from the signer
As mentioned above borrow_global_mut, the variable borrows to the resource Counter under the address, and then performs the +1 operation on the value under the Counter structure.
The following two methods are script methods. What is the difference between them and the above two functions?
public fun : method can be called in any module.
public(script) fun / public entry fun: script function is the entry method in the module, which means that the method can be invoked by initiating a transaction through the console, just like executing a script locally
The next version of Move will replace the public(script) fun with the public entry fun
Self represents its own module.
Use Aptos Cli to compile, deploy and call contracts
# Create a new test environment
aptos init --profile devtest --rest-url --faucet-url
# Compile the move contract
aptos move compile --package-dir my-counter
# Deploy the contract
# For example: aptos move publish --package-dir my-counter --named-addresses basecoin=0x8e00bd9827faf171996ef37f006dd622bb5c3e43ec52298a8f37fd38cd59664 --profile devtest
aptos move publish --package-dir my-counter --named-addresses basecoin= --profile devtest
# call the contract
# For example:
# aptos move run --function-id 0x8e00bd9827faf171996ef37f006dd622bb5c3e43ec52298a8f37fd38cd59664::MyCounter::init_counter --profile devtest
# aptos move run --function-id 0x8e00bd9827faf171996ef37f006dd622bb5c3e43ec52298a8f37fd38cd59664::MyCounter::incr_counter --profile devtest
aptos move run --function-id :::: --profile devtest
# List the modules/resources information of the specified account
aptos account list --query modules --account 0xa1285adb4b8abedf5faf7a46d260c5844f1f64d59dd9b8869db1543cf5bbadf4 --profile devtest
aptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf --profile devtest
Aptos SDK calls the Move contract
After compiling the contract, we can call our contract through sdk.
We can choose to deploy the contract through the sdk, or call the move contract through the sdk.
Deploy contracts through sdk
When we compile, a build/ folder will be generated under the move contract folder
We need to copy the my-counter/build/Examples/bytecode_modules/MyCounter.mv file to the SDK script.
aptos move compile --package-dir my-counter
cp MyCounter.mv my-counter-sdk-demo/
Deploy the sdk code related to the contract
/** Publish a new module to the blockchain within the specified account */
export async function publishModule(accountFrom: AptosAccount, moduleHex: string): Promise
const moudleBundlePayload = new TxnBuilderTypes.TransactionPayloadModuleBundle(
new TxnBuilderTypes.ModuleBundle([new TxnBuilderTypes.Module(new HexString(moduleHex).toUint8Array())]),
);
const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
client.getAccount(accountFrom.address()),
client.getChainId(),
]);
const rawTxn = new TxnBuilderTypes.RawTransaction(
TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
BigInt(sequenceNumber),
moudleBundlePayload,
1000n,
1n,
BigInt(Math.floor(Date.now() / 1000) + 10),
new TxnBuilderTypes.ChainId(chainId),
);
const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);
return transactionRes.hash;
}
Send transactions via SDK
Here, we take init_counter and incr_counter in the my-counter contract as an example.
Construct two methods to call these two methods, so as to realize the function of calling init and incr by the client.
async function initCounter(contractAddress: string, accountFrom: AptosAccount): Promise
const scriptFunctionPayload = new TxnBuilderTypes.TransactionPayloadScriptFunction(
TxnBuilderTypes.ScriptFunction.natural(
`${contractAddress}::MyCounter`,// Contract address::contract name
"init_counter",// script function method
[],
[],
),
);
const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
client.getAccount(accountFrom.address()),
client.getChainId(),
]);
const rawTxn = new TxnBuilderTypes.RawTransaction(
TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
BigInt(sequenceNumber),
scriptFunctionPayload,
1000n,
1n,
BigInt(Math.floor(Date.now() / 1000) + 10),
new TxnBuilderTypes.ChainId(chainId),
);
const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);
return transactionRes.hash;
}
async function incrCounter(contractAddress: string, accountFrom: AptosAccount): Promise
const scriptFunctionPayload = new TxnBuilderTypes.TransactionPayloadScriptFunction(
TxnBuilderTypes.ScriptFunction.natural(
`${contractAddress}::MyCounter`,
"incr_counter",
[],
[],
),
);
const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
client.getAccount(accountFrom.address()),
client.getChainId(),
]);
const rawTxn = new TxnBuilderTypes.RawTransaction(
TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
BigInt(sequenceNumber),
scriptFunctionPayload,
1000n,
1n,
BigInt(Math.floor(Date.now() / 1000) + 10),
new TxnBuilderTypes.ChainId(chainId),
);
const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);
return transactionRes.hash;
}
Obtain the resource information in the account through the SDK.
The resource is stored under the account address to which it belongs, and we can query the relevant resource information according to the account address.
The getCounter() method is actually to get the **Counter** resources under my-counter.
async function getCounter(contractAddress: string, accountAddress: MaybeHexString): Promise
try {
const resource = await client.getAccountResource(
accountAddress.toString(),
`${contractAddress}::MyCounter::Counter`,
);
return (resource as any).data["value"];
} catch (_) {
return "";
}
}
In fact, this effect is similar to that in the sdk
aptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf
final main function
async function main() {
assert(process.argv.length == 3, "Expecting an argument that points to the helloblockchain module");
const contractAddress = "0x173d51b1d50614b03d0c18ffcd958309042a9c0579b6b21fc9efeb48cdf6e0b0"; // Specify the address of the previously deployed contract
const bob = new AptosAccount(); // create a test address Bob
console.log("\n=== Addresses ===");
console.log(`Bob: ${bob.address()}`);
await faucetClient.fundAccount(bob.address(),5_000); // Airdrop 5000 test tokens to Bob's address
console.log("\n=== Initial Balances ===");
console.log(`Bob: ${await accountBalance(bob.address())}`);
await new Promise
readline.question(
"Update the module with Alice's address, build, copy to the provided path, and press enter.",
() => {
resolve();
readline.close();
},
);
});
const modulePath = process.argv[2];
const moduleHex = fs.readFileSync(modulePath).toString("hex");
console.log('Init Counter Moudle.');
let txHash = await initCounter(contractAddress,bob); // init Counter resource under bob, at this time the value of Counter under bob is 0.
await client.waitForTransaction(txHash);
console.log("\n=== Testing Bob Get Counter Value ===");
console.log(`Initial value: ${await getCounter(contractAddress, bob.address())}`);
console.log('========== Incr Counter Value, 1th ==========');
txHash = await incrCounter(contractAddress,bob); // bob calls the incrCounter method once, and the Counter is 1 at this time.
console.log(txHash);
await client.waitForTransaction(txHash);
await Sleep(100);
console.log(`New value: ${await getCounter(contractAddress,bob.address())}`); // Obtain the Counter value under bob's address and output it.
console.log('========== Incr Counter Value, 2th ==========');
txHash = await incrCounter(contractAddress,bob); // bob calls the incrCounter method once, and the Counter is 2.
console.log(txHash);
await client.waitForTransaction(txHash);
await Sleep(100);
console.log(`New value: ${await getCounter(contractAddress,bob.address())}`); // Obtain the Counter value under bob's address and output it.
console.log('========== Incr Counter Value, 3th ==========');
txHash = await incrCounter(contractAddress,bob); // bob calls the incrCounter method once, and the Counter is 3.
console.log(txHash);
await client.waitForTransaction(txHash);
await Sleep(100);
console.log(`New value: ${await getCounter(contractAddress,bob.address())}`); // Obtain the Counter value under bob's address and output it.
}
if (require.main === module) {
main().then((resp) => console.log(resp));
}
Execution effect
The execution is successful. Here, through the SDK, a randomly generated account is inited with a Counter resource (Counter=0), and then incr three times, so the final value of the Counter is 3.
image-20220831200516865
References
References
[1]Using CLI to Run a Local Testnet | Aptos Docs: https://aptos.dev/nodes/local-testnet/using-cli-to-run-a-local-testnet
[2] Source code: https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/move-stdlib/sources/signer.move
