DAOrayaki: Aptos & Move 실제 설명
DAOrayaki DAO 리서치 보너스 풀:
펀딩 주소: DAOrayaki.eth
투표 진행: DAO Reviewer 3/0 통과
연구 유형: Aptos,Layer1
작성자: FF@DoraFactory
이 글은 주로 aptos cli와 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
Aptos 로컬 체인 시작
Using CLI to Run a Local Testnet | Aptos Docs[1]
로컬 체인 시작
추신: 이 방법으로 시작된 로컬 체인과 데이터는 이 명령이 시작된 현재 폴더에 저장되며 .aptos/ 파일에 존재합니다.
aptos node run-local-testnet --with-faucet
성공적으로 시작됨:
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
성공적으로 시작되면 나머지 API 및 수도꼭지 API의 주소를 묻는 메시지가 표시됩니다. 나중에 이 두 가지 정보를 aptos cli 환경에서 구성해야 합니다.
aptos cli 환경 구성
커맨드 라인을 통해 로컬 테스트 체인에 접근하고 호출하기 위해서는 위의 배포 정보에 따라 aptos cli에 대한 config를 구성해야 합니다.
PROFILE=local
aptos init --profile $PROFILE --rest-url --faucet-url
실행하는 동안 다음과 같은 출력이 표시됩니다. 비밀 키를 입력하도록 선택하거나 기본적으로 무작위로 생성할 수 있습니다.
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)]
확인 후 계정이 생성되고 기본 토큰 수로 자금이 조달됩니다.
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"
}
이제부터는 명령에 --profile local을 추가하여 로컬 테스트넷에서 실행할 수 있습니다.
추신: 여기서 --profile은 k8s의 kube-config와 유사하며, 다른 프로필 환경을 설정하고 다른 네트워크를 제어할 수 있습니다.
프로필 구성은 실행기 주소, node-rest-api 및 faucet-api 정보를 설정합니다.
# cli가 제어하는 모든 계정 나열
aptos account list
# 계정 자금:
aptos account fund --profile $PROFILE --account $PROFILE
# 새 자원 계정을 만듭니다.
aptos account create-resource-account --profile $PROFILE --seed 1
# 이동 계약 컴파일
aptos move compile --package-dir hello_blockchain
# 계약 배포
aptos move publish --package-dir hello_blockchain --named-addresses basecoin= --profile local
# 계약을 호출
aptos move run --function-id :::: --profile local
# 지정된 계정의 모듈/리소스 정보 나열
aptos account list --query modules --account 0xa1285adb4b8abedf5faf7a46d260c5844f1f64d59dd9b8869db1543cf5bbadf4 --profile local
aptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf --profile local
# 계약 업그레이드
aptos move publish --upgrade-policy
`arbitrary`, `compatible`,'불변'은 0, 1, 2에 해당합니다.
0은 검사를 수행하지 않고 대체 코드를 강제 적용합니다.
1 호환성 검사를 수행합니다(동일한 공개 함수는 기존 리소스의 메모리 레이아웃을 변경할 수 없음).
2 업그레이드 금지
게시될 때마다 체인의 정책을 이 게시의 정책과 비교합니다(기본값은 1).
이번 정책이 체인의 정책보다 작은 경우에만 계약 업그레이드가 허용됩니다.
간단한 이동 계약 배포
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 소스 코드 분석
모듈은 특정 주소로 게시된 패키지된 함수 및 구조 집합입니다. 스크립트를 사용할 때 게시된 모듈 또는 표준 라이브러리와 함께 실행해야 하며 표준 라이브러리 자체는 0x1 주소로 릴리스된 모듈 그룹입니다.
모듈 MyCounterAddr::MyCounter{ }는 MyCounterAddr 주소 아래에 있습니다(MyCounterAddr ="0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf") 모듈을 만듭니다.
std::signer를 사용하는 것은 표준 라이브러리 아래의 서명자 모듈을 사용하는 것입니다 서명자는 트랜잭션 발신자의 주소를 포함하는 리소스와 유사한 네이티브 복사 불가능한 유형입니다. 서명자 유형을 도입한 이유 중 하나는 발신자 권한이 필요한 기능과 그렇지 않은 기능을 명확히 하기 위해서였습니다. 따라서 함수는 사용자가 해당 리소스에 대한 무단 액세스를 얻도록 속일 수 없습니다. 자세한 내용은 소스 코드 [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,
}
struct를 사용하여 Counter라는 구조체를 정의함과 동시에 key와 store 두 종류의 limiter에 의해 수정된다.
Move의 유형 시스템은 유연하며 각 유형은 네 가지 능력을 정의할 수 있습니다.
유형의 값을 복사, 폐기 및 저장할 수 있는지 여부를 정의합니다.
네 가지 능력 한정자는 Copy, Drop, Store 및 Key입니다.
기능은 다음과 같습니다.
복사 - 값을 복사할 수 있습니다.
삭제 - 범위 끝에서 값을 삭제할 수 있습니다.
키 - 값은 "글로벌 스토리지 작업"에서 키로 액세스할 수 있습니다.
저장 - 값을 전역 상태로 저장할 수 있습니다.
키로 수정하여 여기에 저장한다는 것은 복사, 폐기 또는 재사용이 불가능하지만 안전하게 저장 및 전송할 수 있음을 의미합니다.
능력 구문
기본 유형 및 내장 유형의 기능은 미리 정의되고 변경할 수 없습니다. 정수, 벡터, 주소 및 부울 유형의 값은 본질적으로 복사, 삭제 및 저장 기능이 있습니다.
그러나 다음 구문에 따라 구조의 기능을 추가할 수 있습니다.
struct NAME has ABILITY [, ABILITY] { [FIELDS] }
간단한 라이브러리 예제:
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 {}
}
리소스란?
리소스의 개념은 Move 백서에 자세히 설명되어 있습니다. 초기에는 resource라는 구조체 형태로 구현되었으나, 어빌리티가 도입된 이후에는 Key와 Store의 두 가지 어빌리티를 가진 구조체로 구현되었습니다. 리소스는 안전하게 디지털 자산을 나타낼 수 있으며 복사할 수 없고 폐기하거나 재사용할 수 없지만 안전하게 저장하고 전송할 수 있습니다.
자원의 정의
리소스는 키 및 저장 기능으로 제한된 구조체입니다.
module M {
struct T has key, store {
field: u8
}
}리소스 제한
코드에서 리소스 유형에는 몇 가지 주요 제한 사항이 있습니다.
리소스는 계정에 저장됩니다. 따라서 계정이 할당된 경우에만 존재하며 해당 계정을 통해서만 액세스할 수 있습니다.
계정은 한 번에 특정 유형의 리소스 하나만 보유할 수 있습니다.
리소스는 복사할 수 없으며 이에 대응하는 특수한 종류의 리소스는 제네릭에 관한 장에서 소개한 복사 가능 리소스와 다릅니다. (이것은 Rust의 소유권으로 추상화될 수 있습니다)
리소스를 사용해야 합니다. 즉, 새로 생성된 리소스는 계정으로 이동해야 하며, 계정에서 이동된 리소스는 해체하거나 다른 계정으로 저장해야 합니다.
지금 막 사건
struct Counter has key, store {
value:u64,
}
따라서 usdc와 같은 eth에서 새 자산을 발행해야 하는 경우 견고성과 차이점이 있습니다. 그런 다음 이 자산은 계약의 맵에 기록됩니다. 그러나 이동은 다릅니다. 자산은 사용자의 주소 아래에 리소스로 저장됩니다.
함수 정의
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)
}
정의 형식은 다음과 같습니다.
공개 fun 함수 이름(매개변수: 매개변수 유형) { }
이동 기능은 기본적으로 비공개이며 해당 기능이 정의된 모듈 내에서만 액세스할 수 있습니다. public 키워드는 함수의 기본 가시성을 변경하고 외부에서 액세스할 수 있도록 공개합니다.
init 메소드의 매개변수는 &signer로 계정이 합법적으로 서명된 후에만 호출할 수 있음을 의미하고 move_to는 move의 프리미티브로 카운터 리소스를 게시하고 서명자의 주소에 추가하는 데 사용됩니다. Move의 계정 모델에서 코드와 데이터는 계정 주소 아래에 저장됩니다.
다음은 일반적으로 사용되는 프리미티브 목록입니다.
move_to< T >(&signer,T): 서명자의 주소에 T 유형의 리소스를 게시하고 추가합니다.
move_from< T >(addr: address): T - 주소에서 T 유형의 리소스를 제거하고 이 리소스를 반환합니다.
borrow_global< T >(addr: address): &T - 주소에 있는 T 유형의 리소스에 대한 불변 참조를 반환합니다.
borrow_global_mut< T >(addr: address): &mut T - 주소에 있는 T 유형의 리소스에 대한 변경 가능한 참조를 반환합니다.
exists< T >(주소): bool: 주소 아래에 T 유형의 리소스가 있는지 확인합니다.
incr 메소드의 매개변수는 &signer이기도 합니다. 이는 메소드가 호출되기 전에 계정에 의해 합법적으로 서명되어야 함을 의미합니다.
함수의 반환 값 뒤에 있는 키워드 captures는 이 함수가 획득한 모든 리소스를 명시적으로 정의하는 데 사용됩니다.
Signer::address_of(account) 서명자로부터 주소 가져오기
위에서 언급했듯이, 변수는 주소 아래의 Counter 리소스에 차용한 다음 Counter 구조 아래의 값에 대해 +1 연산을 수행합니다.
다음 두 가지 방법은 스크립트 방법인데 위의 두 가지 기능과 다른 점은 무엇입니까?
public fun : 모든 모듈에서 메서드를 호출할 수 있습니다.
public(script) fun / public entry fun: 스크립트 함수는 모듈의 진입 방법으로, 로컬에서 스크립트를 실행하는 것처럼 콘솔을 통해 트랜잭션을 시작하여 메서드를 호출할 수 있음을 의미합니다.
Move의 다음 버전은 public(script) fun을 public entry fun으로 대체할 것입니다.
Self는 자체 모듈을 나타냅니다.
Aptos Cli를 사용하여 계약 컴파일, 배포 및 호출
# 새로운 테스트 환경 만들기
aptos init --profile devtest --rest-url --faucet-url
# 이동 계약 컴파일
aptos move compile --package-dir my-counter
# 계약 배포
# 예: 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
# 계약을 호출
# 예를 들어:
# 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
# 지정된 계정의 모듈/리소스 정보 나열
aptos account list --query modules --account 0xa1285adb4b8abedf5faf7a46d260c5844f1f64d59dd9b8869db1543cf5bbadf4 --profile devtest
aptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf --profile devtest
Aptos SDK는 Move 컨트랙트를 호출합니다.
계약을 컴파일한 후 sdk를 통해 계약을 호출할 수 있습니다.
SDK를 통해 계약을 배포하거나 SDK를 통해 이동 계약을 호출하도록 선택할 수 있습니다.
SDK를 통해 계약 배포
컴파일하면 이동 계약 폴더 아래에 build/ 폴더가 생성됩니다.
my-counter/build/Examples/bytecode_modules/MyCounter.mv 파일을 SDK 스크립트에 복사해야 합니다.
aptos move compile --package-dir my-counter
cp MyCounter.mv my-counter-sdk-demo/
컨트랙트 관련 SDK 코드 배포
/** 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;
}
SDK를 통해 거래 보내기
여기에서는 my-counter 계약의 init_counter 및 incr_counter를 예로 들어 보겠습니다.
이 두 가지 방법을 호출하는 두 가지 방법을 구성하여 클라이언트에서 init 및 incr 호출 기능을 실현하십시오.
async function initCounter(contractAddress: string, accountFrom: AptosAccount): Promise
const scriptFunctionPayload = new TxnBuilderTypes.TransactionPayloadScriptFunction(
TxnBuilderTypes.ScriptFunction.natural(
`${contractAddress}::MyCounter`,// 계약 주소::계약명
"init_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;
}
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;
}
SDK를 통해 계정의 리소스 정보를 가져옵니다.
리소스는 해당 리소스가 속한 계정 주소에 저장되며 계정 주소에 따라 관련 리소스 정보를 조회할 수 있습니다.
getCounter() 메서드는 실제로 my-counter 아래의 **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 "";
}
}
사실 이 효과는 sdk의 효과와 비슷합니다.
aptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf
최종 주요 기능
async function main() {
assert(process.argv.length == 3, "Expecting an argument that points to the helloblockchain module");
const contractAddress = "0x173d51b1d50614b03d0c18ffcd958309042a9c0579b6b21fc9efeb48cdf6e0b0"; // 이전에 배포한 컨트랙트의 주소 지정
const bob = new AptosAccount(); // 테스트 주소 생성 Bob
console.log("\n=== Addresses ===");
console.log(`Bob: ${bob.address()}`);
await faucetClient.fundAccount(bob.address(),5_000); // 5000 테스트 토큰을 Bob의 주소로 Airdrop
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); // bob 아래에서 Counter 리소스를 초기화합니다. 이때 bob 아래의 Counter 값은 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은 incrCounter 메서드를 한 번 호출하고 이때 Counter는 1입니다.
console.log(txHash);
await client.waitForTransaction(txHash);
await Sleep(100);
console.log(`New value: ${await getCounter(contractAddress,bob.address())}`); // bob의 주소 아래 Counter 값을 구해서 출력한다.
console.log('========== Incr Counter Value, 2th ==========');
txHash = await incrCounter(contractAddress,bob); // bob은 incrCounter 메서드를 한 번 호출하고 Counter는 2입니다.
console.log(txHash);
await client.waitForTransaction(txHash);
await Sleep(100);
console.log(`New value: ${await getCounter(contractAddress,bob.address())}`); // bob의 주소 아래 Counter 값을 구해서 출력한다.
console.log('========== Incr Counter Value, 3th ==========');
txHash = await incrCounter(contractAddress,bob); // bob은 incrCounter 메서드를 한 번 호출하고 Counter는 3입니다.
console.log(txHash);
await client.waitForTransaction(txHash);
await Sleep(100);
console.log(`New value: ${await getCounter(contractAddress,bob.address())}`); // bob의 주소 아래 Counter 값을 구해서 출력한다.
}
if (require.main === module) {
main().then((resp) => console.log(resp));
}
실행 효과
실행에 성공하면 SDK를 통해 임의로 생성된 계정이 Counter 리소스(Counter=0)로 초기화된 후 3번 증가하므로 Counter의 최종 값은 3이 됩니다.
image-20220831200516865
참조
참조
[1]Using CLI to Run a Local Testnet | Aptos Docs: https://aptos.dev/nodes/local-testnet/using-cli-to-run-a-local-testnet
[2] 소스 코드: https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/move-stdlib/sources/signer.move


