この記事は「Qiita夏祭り2020」の「「会計」「勤怠」をハックしよう!freee API のTips募集」の参加記事です。
詳細は下記を御覧ください。
▼Qiita夏祭り
https://qiita.com/summer-festival
▼「会計」「勤怠」をハックしよう!freee API のTips募集
https://qiita.com/official-events/3f740f71bbdcb59d9959
はじめに
どーも、のぶこふです。
今回は「Qiita夏祭り2020」ということで、私が普段から触っているブロックチェーン(Corda)と連携させてみようと思います。
「freee × Corda」です。
「freee API」を使用し、ブロックチェーンで改ざん不能なアクセスログを溜めていくような動きにしようと思います。
※freeeとは?Cordaとは?というのは、割愛させていただきます。
※本記事はサンプルですので、ブロックチェーンとDBとの整合性はどうやって担保するの?とか言わないでください。
環境
- Windows10
- ローカル環境に、Cordaのノードを起動させます
※Windowsのローカルでもサクッと手軽に環境構築できるのはCordaの強みだと思う
- ローカル環境に、Cordaのノードを起動させます
- freee API
- Corda OSS 4.5
- 実現するなら1ノードだけでも問題は無いのですが、改ざん耐性を上げるため、複数ノード+Notaryとして、それっぽい感じにしています
- SpringBoot
- もれなくCordaについてくる
- Bootstrap
- 画面周りを特に考えたくなかったので、サクッと実現するため
- Kotlin
- Cordaをやるなら、やっぱりKotlin
freee API スタート
公式の手順通りに進めていきます。
https://developer.freee.co.jp/getting-started
1.セットアップ
▼freeeアカウントを取得
私は、アカウントを持っていないので、アカウントの取得から始めます。
すでに取得済みの方は、スキップしてください。
下記リンクにて、メールアドレス/パスワードを設定して、利用開始ボタンを押下します。
https://app.secure.freee.co.jp/developers/start_guides/new_user
数分後、指定したメールアドレスに「[freee API] 開発用テスト事業所の作成完了のお知らせ」という件名のメールが届くので、「アクセストークン取得ページへ」ボタンを押下します。
メール認証を実施すると、「[freee] メール認証キー送信のお知らせ」という件名のメールが届くので、メールに記載されているリンクを押下して、メール認証を完了させます。
メール認証完了後、「アクセストークンを取得する」ボタンを押下すると、トークン発行ページへ遷移します。
2.APIを叩いてみる
▼API Call(ブラウザから)
-
アクセストークンをコピーします
-
会計APIリファレンスの事業所一覧取得に移動して、右側にある鍵アイコンを押下します。
3.アプリケーション連携を実装する
※ソースコードの全量は私のGithubにあげてあります。使用する際は、下記の「1. テンプレートダウンロード」「2. ファイル作成&修正」は実施不要です。「3. 稼働確認」へ進んでください。
1. テンプレートダウンロード
git clone https://github.com/corda/cordapp-template-kotlin.git && cd cordapp-template-kotlin
▼Gitが入っていない方はコチラからダウンロード→インストール
https://gitforwindows.org/
※コマンドを使わずに、テンプレを直接DLしてくるのでも可
2. ファイル作成&修正
2.1. TemplateSchema.kt
- 下記フォルダを新規作成します
- cordapp-template-kotlin/contracts/src/main/kotlin/com/template/schemas/
- ファイルも併せて新規作成します
- cordapp-template-kotlin/contracts/src/main/kotlin/com/template/schemas/TemplateSchema.kt
TemplateSchema.kt
package com.template.schemas
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Table
object TemplateSchema
object TemplateSchemaV1 : MappedSchema(
schemaFamily = TemplateSchema.javaClass,
version = 1,
mappedTypes = listOf(TemplateSchemaV1.PersistentTemplate::class.java)
) {
@Entity
@Table(name = "template_state")
class PersistentTemplate(
@Column(name = "issuer")
var issuer: String,
@Column(name = "counterParty")
var counterParty: String,
@Column(name = "id")
var id: String,
@Column(name = "date")
var date: String,
@Column(name = "action")
var action: String,
@Column(name = "data")
var data: String
) : PersistentState() {
constructor() : this("", "", "", "", "", "")
}
}
2.2. TemplateState.kt
- 下記ファイルを修正します
- cordapp-template-kotlin/contracts/src/main/kotlin/com/template/states/TemplateState.kt
TemplateState.kt
package com.template.states
import com.template.contracts.TemplateContract
import com.template.schemas.TemplateSchemaV1
import net.corda.core.contracts.BelongsToContract
import net.corda.core.contracts.LinearState
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState
// *********
// * State *
// *********
@BelongsToContract(TemplateContract::class)
data class TemplateState(
val issuer: Party,
val counterParty: Party,
val id: String,
val date: String,
val action: String,
val data: String,
override val linearId: UniqueIdentifier = UniqueIdentifier(),
override val participants: List<AbstractParty> = listOf(issuer, counterParty)
) : LinearState, QueryableState {
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) {
is TemplateSchemaV1 -> TemplateSchemaV1.PersistentTemplate(this.issuer.name.toString(), this.counterParty.name.toString(), this.id, this.date, this.action, this.data)
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(TemplateSchemaV1)
}
2.3. Flows.kt
- 下記ファイルを修正します
- cordapp-template-kotlin/workflows/src/main/kotlin/com/template/flows/Flows.kt
Flow.kt
package com.template.flows
import co.paralleluniverse.fibers.Suspendable
import com.template.contracts.TemplateContract
import com.template.states.TemplateState
import net.corda.core.contracts.Command
import net.corda.core.contracts.requireThat
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
// *********
// * Flows *
// *********
@InitiatingFlow
@StartableByRPC
class Initiator(private val counterParty: Party, private val id: String, private val action: String, private val date: String, private val data: String) : FlowLogic<SignedTransaction>() {
override val progressTracker = ProgressTracker()
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
// Create Output
val output = TemplateState(issuer = ourIdentity, counterParty = counterParty, id = this.id, action = this.action, date = this.date, data = this.data)
// Create command
val cmd = Command(TemplateContract.Commands.Action(), listOf(ourIdentity.owningKey, counterParty.owningKey))
// Create Transaction
val txBuilder = TransactionBuilder(notary)
.addOutputState(output)
.addCommand(cmd)
txBuilder.verify(serviceHub)
// Signing Transaction
val signedTx = serviceHub.signInitialTransaction(txBuilder)
// Gathering Signs
val counterPartySession = initiateFlow(counterParty)
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, setOf(counterPartySession)))
// Finalize Transaction
return subFlow(FinalityFlow(fullySignedTx, setOf(counterPartySession)))
}
}
@InitiatedBy(Initiator::class)
class Responder(val counterPartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val signTransactionFlow = object : SignTransactionFlow(counterPartySession) {
override fun checkTransaction(stx: SignedTransaction) = requireThat {}
}
val txId = subFlow(signTransactionFlow).id
subFlow(ReceiveFinalityFlow(counterPartySession, expectedTxId = txId))
}
}
2.4. constants.properties
- Corda4.5を使用するため、下記ファイルを修正します。
- cordapp-template-kotlin/constants.properties
cordaReleaseGroup=net.corda
cordaCoreReleaseGroup=net.corda
cordaVersion=4.5 # 4.4 -> 4.5
cordaCoreVersion=4.5 # 4.4 -> 4.5
2.5. Contoroller.kt
- クライアントサイドを修正します
- cordapp-template-kotlin/clients/src/main/kotlin/com.template/webserver/Contoroller.kt
- freeeAPIもここで叩いています
Controller.kt
package com.template.webserver
import com.google.gson.Gson
import com.template.flows.Initiator
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.messaging.startFlow
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.util.MultiValueMap
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.client.RestTemplate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Controller
class Controller(rpc: NodeRPCConnection) {
private val proxy = rpc.proxy
private val INDEX: String = "index"
private val URL: String = "https://api.freee.co.jp"
private val VERSION: String = "/api/1/"
private val rt: RestTemplate = RestTemplate();
@RequestMapping(value = "/")
fun index(): String {
return INDEX
}
@RequestMapping(value = "/getCompanies", method = [RequestMethod.POST])
fun getCompanies(@RequestBody body: MultiValueMap<String, String>, model: Model): String {
val token = body.getFirst("token")
val header = HttpHeaders()
header.add("Authorization", "Bearer " + token)
header.add("accept", "application/json")
val req = HttpEntity<String>(header)
// ログインユーザ情報を取得
val USERS_ME: String = "users/me"
val userJson = rt.exchange(URL + VERSION + USERS_ME, HttpMethod.GET, req, String::class.java)
val gson = Gson()
val json = gson.fromJson(userJson.body, Users::class.java)
// 事業所情報を取得
val COMPANIES: String = "companies"
val response = rt.exchange(URL + VERSION + COMPANIES, HttpMethod.GET, req, String::class.java)
// counterParty
val x500Name = CordaX500Name.parse("O=PartyB,L=New York,C=US")
val counterParty = proxy.wellKnownPartyFromX500Name(x500Name) as Party
// date
val current = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val formatted = current.format(formatter)
// Flow Start
val result = proxy.startFlow(
::Initiator,
counterParty,
json.user.display_name, // id
"get companies", // action
formatted.toString(), // date
response.body // data
).returnValue.getOrThrow()
when (result) {
is SignedTransaction -> {
model.addAttribute("code", "200")
model.addAttribute("data", response.body)
}
else -> model.addAttribute("error", "400")
}
return INDEX
}
}
data class User(
val id: String,
val email: String,
val display_name: String,
val first_name: String,
val last_name: String,
val first_name_kana: String,
val last_name_kana: String
)
data class Users(
val user: User
)
2.6. index.html
- templatesを作成し、index.htmlを移動します
- cordapp-template-kotlin/clients/src/main/resources/templates/index.html
index.html
<!DOCTYPE html>
<html lang="jp" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Example front-end.</title>
<script src="webjars/jquery/3.4.1/jquery.min.js"></script>
<link rel="stylesheet" href="webjars/bootstrap/4.3.1/css/bootstrap.min.css">
<script src="webjars/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</head>
<body>
<div th:if="${code != null}" class="alert alert-primary" role="alert">
<p th:text="${code}"></p>
</div>
<div th:if="${error != null}" class="alert alert-warning" role="alert">
<p th:text="${error}"></p>
</div>
<div class="container">
<div class="row">
<div class="col-sm">
<div class="card">
<div class="card-header"></div>
<div class="card-body">
<form th:action="getCompanies" method="post">
<div class="form-group row">
<label for="token">token</label>
<input type="text" class="form-control" name="token" id="token" required>
</div>
<button type="submit" class="btn btn-primary">事業所取得</button>
</form>
</div>
</div>
</div>
</div>
<th:block th:if="${data != null}" class="alert alert-primary" role="alert">
<div class="row">
<div class="col-sm">
<div class="card">
<div class="card-header">事業所</div>
<div class="card-body">
<p th:text="${data}"></p>
</div>
</div>
</div>
</div>
</th:block>
</div>
</body>
</html>
3. 稼働確認
では、実際に動かしてみましょう。
- ビルド
※JDK9以上はNGです。おそらくJREも必要になります。JDK9以上の方は、自己責任のもと、下記からJDK8をダウンロード→インストールしてください。
私は、ここで半日躓きました・・・( ´Д`)=3
▼Java SE 8u251
https://www.oracle.com/java/technologies/javase-downloads.html#JDK8
gradlew.bat deployNodes
- ノード起動
build\nodes\runnodes.bat
- フロントサイドも確認してみます
- コマンド実行後、「localhost:10050」にアクセスします。
gradlew.bat runTemplateServer
- 入力フォームに、freeeのアクセストークンを入力し、「事業所取得」ボタンを押下します。
- 正常に終了したらJSON形式で出力されます
- ※エラーハンドリングは実施してません
- ※アクセストークンの有効期限は1日らしいので、切れた場合は再取得してください
- Corda側も確認してみます
- NodeAを開きます
- コマンドです
run vaultQuery contractStateType: com.template.states.TemplateState
- 実行結果
- 2回「事業所取得」ボタンを押下した内容が登録されています
名前 | 結果 |
---|---|
id | ログインユーザ情報(のぶこふ) |
date | 「事業所取得」ボタンを押下したタイミング |
action | 「事業所取得」 |
data | 「事業所取得」の結果(JSON形式) |
>>> run vaultQuery contractStateType: com.template.states.TemplateState
states:
- state:
data: !<com.template.states.TemplateState>
issuer: "O=PartyA, L=London, C=GB"
counterParty: "O=PartyB, L=New York, C=US"
id: "のぶこふ"
date: "2020-07-03 17:08:01"
action: "get companies"
data: "{\"companies\":[{\"id\":2525935,\"name\":null,\"name_kana\":null,\"display_name\"\
:\"開発用テスト事業所\",\"role\":\"admin\"}]}"
linearId:
externalId: null
id: "535345ea-a113-4a58-bd7e-981b34ef2d51"
participants:
- "O=PartyA, L=London, C=GB"
- "O=PartyB, L=New York, C=US"
contract: "com.template.contracts.TemplateContract"
notary: "O=Notary, L=London, C=GB"
encumbrance: null
constraint: !<net.corda.core.contracts.SignatureAttachmentConstraint>
key: "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTEw3G5d2maAq8vtLE4kZHgCs5jcB1N31cx1hpsLeqG2ngSysVHqcXhbNts6SkRWDaV7xNcr6MtcbufGUchxredBb6"
ref:
txhash: "3E0C328B5486C9E77FB7F4AB3B5192D406C3727BE3A050CA08D6F4A32AB0D482"
index: 0
- state:
data: !<com.template.states.TemplateState>
issuer: "O=PartyA, L=London, C=GB"
counterParty: "O=PartyB, L=New York, C=US"
id: "のぶこふ"
date: "2020-07-03 17:08:14"
action: "get companies"
data: "{\"companies\":[{\"id\":2525935,\"name\":null,\"name_kana\":null,\"display_name\"\
:\"開発用テスト事業所\",\"role\":\"admin\"}]}"
linearId:
externalId: null
id: "31a4b1a0-edef-48d5-abc1-765c6403f3bd"
participants:
- "O=PartyA, L=London, C=GB"
- "O=PartyB, L=New York, C=US"
contract: "com.template.contracts.TemplateContract"
notary: "O=Notary, L=London, C=GB"
encumbrance: null
constraint: !<net.corda.core.contracts.SignatureAttachmentConstraint>
key: "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTEw3G5d2maAq8vtLE4kZHgCs5jcB1N31cx1hpsLeqG2ngSysVHqcXhbNts6SkRWDaV7xNcr6MtcbufGUchxredBb6"
ref:
txhash: "5A0FA9D245797FA40D54533D61144E397F764C57A46D465EA4AEEC0ABAAE1474"
index: 0
statesMetadata:
- ref:
txhash: "3E0C328B5486C9E77FB7F4AB3B5192D406C3727BE3A050CA08D6F4A32AB0D482"
index: 0
contractStateClassName: "com.template.states.TemplateState"
recordedTime: "2020-07-03T08:08:08.231Z"
consumedTime: null
status: "UNCONSUMED"
notary: "O=Notary, L=London, C=GB"
lockId: null
lockUpdateTime: null
relevancyStatus: "RELEVANT"
constraintInfo:
constraint:
key: "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTEw3G5d2maAq8vtLE4kZHgCs5jcB1N31cx1hpsLeqG2ngSysVHqcXhbNts6SkRWDaV7xNcr6MtcbufGUchxredBb6"
- ref:
txhash: "5A0FA9D245797FA40D54533D61144E397F764C57A46D465EA4AEEC0ABAAE1474"
index: 0
contractStateClassName: "com.template.states.TemplateState"
recordedTime: "2020-07-03T08:08:14.420Z"
consumedTime: null
status: "UNCONSUMED"
notary: "O=Notary, L=London, C=GB"
lockId: null
lockUpdateTime: null
relevancyStatus: "RELEVANT"
constraintInfo:
constraint:
key: "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTEw3G5d2maAq8vtLE4kZHgCs5jcB1N31cx1hpsLeqG2ngSysVHqcXhbNts6SkRWDaV7xNcr6MtcbufGUchxredBb6"
totalStatesAvailable: -1
stateTypes: "UNCONSUMED"
otherResults: []
4. おわりに
いかがだったでしょうか。
すごく簡単な実装ではありますが「実行ログをブロックチェーンに書き込む」といったものを作成してみました。
※本来であれば、ブロックチェーンに載せる情報、CordaであればTxチェーンの構造をどうするか等を検討する必要がありますが、雰囲気を感じてもらうために、このような実装、及び表現としていることをご了承ください。
今回は、事業所一覧を取得するボタン押下のみ配置していますが、他APIとも連携することで、既存のアプリケーションログよりも改ざん耐性の強いログを作成できるかと思います。
なお、ブロックチェーンの有効な活用方法としては複数社間でのやり取りがメインですが、今回のように1社のみで改ざん耐性やトレーサビリティを追求するのであれば、ガートナーのレポートにもあったようにQLDB、BlockchainTable、ScalarといったLDBMSのほうが都合が良かったりします。そのへんの話はおいおい。。。しないかな。
初のQiita祭りに参加ということで、諸々形式に則っていれていない箇所もあるかもしれませんが、生暖かい目で見守っていただければと思います。
今回はここまでです。
ありがとうございました。