🎉

AikenでCardanoスマートコントラクトを書こう

2023/03/26に公開

開発環境のセットアップ

Aikenプロジェクトの作成

Aiken new コマンドと構成

aiken new aiken-lang/hello_world
cd hello_world
hello_world
./hello_world
├── README.md
├── aiken.toml
├── lib
│   └── hello_world
└── validators

標準ライブラリの使用

続いて、ルートディレクトリに作成されたaiken.tomlファイルを見てみましょう。

aiken.toml
name = "aiken-lang/hello_aiken"
version = "0.0.0"
license = "Apache-2.0"
description = "Aiken contracts for project 'ciel/hello_aiken'"

[repository]
user = "aiken-lang"
project = "hello_world"
platform = "github"

[[dependencies]]
name = "aiken-lang/stdlib"
version = "main"
source = "github"

依存関係のダウンロード

aiken check コマンドを使用することで、型チェックとテストを実行することができます。実行してみましょう!

$ aiken check

---
Resolving versions
  Downloading packages
   Downloaded 1 package in 1.12s
    Compiling aiken-lang/stdlib main (/Users/yoshiki/Desktop/aiken/hello_world/build/packages/aiken-lang-stdlib)
    Compiling aiken-lang/hello_world 0.0.0 (/Users/yoshiki/Desktop/aiken/hello_world)

Summary
    0 errors, 0 warnings
---

今回はまだ依存関係のライブラリ(現時点ではaiken-lang/stdlibのみ)がダウンロードされていないので、ここでダウンロードされています。
続いてaiken-lang/stdlib mainaiken-lang/hello_worldが方チェックされ、エラーがないことが確認できました。

続けてaiken checkを実行するとダウンロードは行われず、テストのみが実行されると思います。
このように、Aikenでは、開発に変更が加えられるたびにこまめにaiken checkコマンドで型チェックを実行することで、コンパイルできるコードであることを確かめることができます。

validatorの作成

さて、ここからはお待ちかね Aiken のコードを実際に書いていきます!

まずはvalidatorsディレクトリ配下に、hello_world.akを作成します。Aikenファイルの拡張子は.akです。

$ touch validators/hello_world.ak

関数と型のインポート

作成したhello_world.akに以下のように記述します。

validators/hello_world.ak
+ use aiken/hash.{Blake2b_224, Hash}
+ use aiken/list
+ use aiken/transaction.{ScriptContext}
+ use aiken/transaction/credential.{VerificationKey}

まずはuseキーワドを使用し、 aiken が持つ関数と型をインポートします。

独自の型を定義する

AikenにはBool,Int,ByteArray,List,Tuples,Voidの6つの型が言語に組み込まれていますが、開発中に独自の型が必要になる場合があります。
その時はtypeキーワードを使用し、以下のように独自の型を宣言することができます。

validators/hello_world.ak
use aiken/hash.{Blake2b_224, Hash}
use aiken/list
use aiken/transaction.{ScriptContext}
use aiken/transaction/credential.{VerificationKey}

+ type Datum {
+   owner: Hash<Blake2b_224, VerificationKey>,
+ }
+  
+ type Redeemer {
+   msg: ByteArray,
+ }

validator関数の宣言

validators/hello_world.ak
use aiken/hash.{Blake2b_224, Hash}
use aiken/list
use aiken/transaction.{ScriptContext}
use aiken/transaction/credential.{VerificationKey}

type Datum {
  owner: Hash<Blake2b_224, VerificationKey>,
}
 
type Redeemer {
  msg: ByteArray,
}

+ validator {
+   fn hello_world(datum: Datum, redeemer: Redeemer, context: ScriptContext) -> Bool {
+     let must_say_hello = redeemer.msg == "Hello, World!"
+  
+     let must_be_signed =
+       list.has(context.transaction.extra_signatories, datum.owner)
+  
+     must_say_hello && must_be_signed
+   }
+ }

aiken build でビルド

validatorが完成したので最後にaiken buildを実行し、ビルドしましょう!

$ aiken build

---
Compiling aiken-lang/stdlib main (/Users/yoshiki/Desktop/aiken/hello_world/build/packages/aiken-lang-stdlib)
    Compiling aiken-lang/hello_world 0.0.0 (/Users/yoshiki/Desktop/aiken/hello_world)
   Generating project's blueprint (/Users/yoshiki/Desktop/aiken/hello_world/plutus.json)

Summary
    0 errors, 0 warnings
---

ログを確認するとaiken-lang/hello_worldが正常にコンパイルされたことがわかります。
エラーはありませんでした!素晴らしいです!🥳

aiken buildコマンドでは、コンパイルが通るとルートディレクトリにplutus.jsonが作成されます。これはCIP-0057で現在議論されている、「Plutusにおけるそのコントラクトの検証ルール等が記載された仕様書」であり、Aikenでは既にこの仕様書が完全に組み込まれているので、ビルドした際に自動で生成されるようになっています。

Lucid

Preview Testnetアドレス作成とtADAの取得

スマートコントラクトを使用するためにはアドレスと資金を用意する必要があります。
今回は実験的な開発であるため、Cardano のテストネットである Preview Testnet のアドレスと tADA(testnet用のADA)を用意していきす。

ルートディレクトリにgenerate-credentials.tsという名前でTypescriptのファイルを作成します。

$ touch generate-credentials.ts

generate-credentials.ts に以下を記述します。

./generate-credentials.ts
import { Lucid } from "https://deno.land/x/lucid@0.8.3/mod.ts";
 
const lucid = await Lucid.new(undefined, "Preview");
 
const privateKey = lucid.utils.generatePrivateKey();
await Deno.writeTextFile("key.sk", privateKey);
 
const address = await lucid
  .selectWalletFromPrivateKey(privateKey)
  .wallet.address();
await Deno.writeTextFile("key.addr", address);

ルードディレクトリにいることを確認し、Deno runコマンドを実行します。
実行すると秘密鍵が書かれたkey.skと、その公開鍵(アドレス)が書かれたkey.addrが生成されます。

$ deno run --allow-net --allow-write generate-credentials.ts

テストネット用のADA(tADA)は、Testnets faucetで取得できます。

実際にコントラクトを使用してみよう

ここまでの流れで、コントラクトの実行に必要な「オンチェーンコード」・「送受信用のアドレス」・「tADA」が揃ったので実際にスマートコントラクトを実行してみましょう!

ルートディレクトリにhello_world-lock.tsを作成し、必要な関数のインポートと Lucid の初期化を行います。

  • BLOCKFROST_API_KEYの部分をご自身の(Blockfrost)[]の PROJECT ID に置き換えてください。
./hello_world-lock.ts
import {
  Blockfrost,
  C,
  Constr,
  Data,
  Lucid,
  SpendingValidator,
  TxHash,
  fromHex,
  toHex,
  utf8ToHex,
} from "https://deno.land/x/lucid@0.8.3/mod.ts";
import * as cbor from "https://deno.land/x/cbor@v1.4.1/index.js";

// "BLOCKFROST_API_KEY" の部分をご自身の Blockfrost のAPIキーに置き換えてください。

const lucid = await Lucid.new(
  new Blockfrost(
    "https://cardano-preview.blockfrost.io/api/v0",
    "BLOCKFROST_API_KEY"
  ),
  "Preview"
);
./hello_world-lock.ts
lucid.selectWalletFromPrivateKey(await Deno.readTextFile("./key.sk"));
 
const validator = await readValidator();
 
// --- Supporting functions
 
async function readValidator(): Promise<SpendingValidator> {
  const validator = JSON.parse(await Deno.readTextFile("plutus.json")).validators[0];
  return {
    type: "PlutusV2",
    script: toHex(cbor.encode(fromHex(validator.compiledCode))),
  };
}
./hello_world-lock.ts
const publicKeyHash = lucid.utils.getAddressDetails(
  await lucid.wallet.address()
).paymentCredential?.hash;
 
const datum = Data.to(new Constr(0, [publicKeyHash]));
 
const txHash = await lock(1000000n, { into: validator, owner: datum });
 
await lucid.awaitTx(txHash);
 
console.log(`1 tADA locked into the contract at:
    Tx ID: ${txHash}
    Datum: ${datum}
`);
 
// --- Supporting functions
 
async function lock(
  lovelace: bigint,
  { into, owner }: { into: SpendingValidator; owner: string }
): Promise<TxHash> {
  const contractAddress = lucid.utils.validatorToAddress(into);
 
  const tx = await lucid
    .newTx()
    .payToContract(contractAddress, { inline: owner }, { lovelace })
    .complete();
 
  const signedTx = await tx.sign().complete();
 
  return signedTx.submit();
}

deno runコマンドで実行してみましょう。

$ deno run --allow-net --allow-read --allow-env hello_world-lock.ts

# ターミナルの戻り値
---
1 tADA locked into the contract at:
    Tx ID: b05c0b203beb09dcb8705bd1c3b392f6f478774a2a2cab2b20bc261a73a6b987
    Datum: d8799f581c2fc5df6a7876b5b45930f14ff5df44cf959e2ce421af2add68385a93ff
---

Discussion