Waves - RIDE4DApps (RIDE for dApps) でエスクロー (escrow) してみる

RIDE for dAppsにて単一トランザクションでアセットの預託と条件付けを行えるようになったので、
ブロックチェーンでよくあるアレをWaves Platformにおいて考える。

何ができれば良いか

  • ハッシュロックで条件付け
  • 預託時点で送付相手とその内容は決まっている
  • 相手側が受け取るか、受け取らなかった場合は回収(キャンセル)するのいずれかができる

RIDE for dApps

大体以下な感じになる。

{-# STDLIB_VERSION 3 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ACCOUNT #-}

func unlock(id: String, preimage: ByteVector, heightKey: String, addressKey: String) = {
    # DataEntry
    let unlockHeight = extract(getInteger(this, id + heightKey))
    let condition = extract(getBinary(this, id + "_condition"))
    let destinationAddress = extract(getBinary(this, id + addressKey))
    let amount = extract(getInteger(this, id + "_amount"))
    let assetId = extract(getBinary(this, id + "_asset"))
    let asset = if size(assetId) > 0 then assetId else unit

    let flag = match getBoolean(this, id) {
        case b: Boolean => b
        case _ => false
    }

    if (flag && (height >= unlockHeight)) then {
        if (condition == sha256(preimage)) then {
            ScriptResult(
                    WriteSet([DataEntry(id, false)]),
                    TransferSet([ScriptTransfer(Address(destinationAddress), amount, asset)])
                )        
        }
        else {
            throw("unlock failure")
        }
    }
    else {
        throw("invalid id")
    }
}

@Callable(i)
func receive(id: String, preimage: ByteVector) = {
    unlock(id, preimage, "_receive_height", "_receiver")
}

@Callable(i)
func cancel(id: String, preimage: ByteVector) = {
    unlock(id, preimage, "_cancel_height", "_sender")
}

@Callable(i)
func escrow(id: String, receiveHeight: Int, cancelHeight: Int, condition: ByteVector, receiverAddress: ByteVector) = {
    # DataEntry Key
    let sender = id + "_sender"
    let receiver = id + "_receiver"
    let rh = id + "_receive_height"
    let ch = id + "_cancel_height"
    let cond = id + "_condition"
    let amount = id + "_amount"
    let asset = id + "_asset"

    let payment = extract(i.payment)
    let assetId = if (isDefined(payment.assetId)) then extract(payment.assetId) else base58''

    let flag = match getBoolean(this, id) {
        case b: Boolean => b
        case _ => false
    }
    if (flag) then throw("invaild id")
    else {
        WriteSet([
            DataEntry(id, true),
            DataEntry(sender, i.caller.bytes),
            DataEntry(receiver, receiverAddress),
            DataEntry(rh, receiveHeight),
            DataEntry(ch, cancelHeight),
            DataEntry(cond, condition),
            DataEntry(amount, payment.amount),
            DataEntry(asset, assetId)
        ])
    }
}

コンパイルは通ったが試してはいない。動きそうではある。
(省略してあるが、@Verifier関数を実装してないと変更できなくなる)
SmartAccountの時点でも実現はできたはずだが、だいぶ書きやすくなったかも?
呼び出すほうもInvokeScriptするだけだしね。

その他、
DataEntryの構成は、構造化データの読み書きが現状できないのでダラダラと書いてあるが、(やれる範囲で)固定長の文字列なりにパックしたほうが良いかもしれない。
使いまわせるように一意なプリフィックスを付与して個別のエスクローを識別しているが、この辺は呼び出しトランザクションのIDを取得(現状できないが)できるとたぶん楽。

細かい話はキリがないので、以上。


See also