├── img ├── ictc.jpg ├── ictc-ta.png ├── ictc-2pc.png └── ictc-saga.png ├── vessel.dhall ├── .gitignore ├── canister_ids.json ├── dfx.json ├── src ├── TATypes.mo ├── CallType.mo ├── TA.mo └── SagaTM.mo ├── package-set.dhall ├── README-CN.md ├── README.md ├── docs ├── ictc_reference.md ├── ictc_reference-2.0.md ├── ictc_reference-1.5.md └── ictc_reference-3.0.md └── examples ├── Example2PC.mo └── Example.mo /img/ictc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iclighthouse/ICTC/HEAD/img/ictc.jpg -------------------------------------------------------------------------------- /img/ictc-ta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iclighthouse/ICTC/HEAD/img/ictc-ta.png -------------------------------------------------------------------------------- /img/ictc-2pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iclighthouse/ICTC/HEAD/img/ictc-2pc.png -------------------------------------------------------------------------------- /img/ictc-saga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iclighthouse/ICTC/HEAD/img/ictc-saga.png -------------------------------------------------------------------------------- /vessel.dhall: -------------------------------------------------------------------------------- 1 | { 2 | dependencies = [ "base", "icl" ], 3 | compiler = None Text 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist-ssr 3 | *.local 4 | .dfx 5 | .vessel 6 | .config 7 | .tmp 8 | 9 | # Various IDEs and Editors 10 | .vscode/ 11 | .idea/ 12 | .history/ 13 | **/*~ 14 | 15 | # frontend code 16 | node_modules/ 17 | dist/ -------------------------------------------------------------------------------- /canister_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "Example": { 3 | "ic": "bgwjo-yyaaa-aaaak-aahsq-cai" 4 | }, 5 | "Example2PC": { 6 | "ic": "rvnbq-sqaaa-aaaak-acfhq-cai" 7 | }, 8 | "TokenA": { 9 | "ic": "ueghb-uqaaa-aaaak-aaioa-cai" 10 | }, 11 | "TokenB": { 12 | "ic": "udhbv-ziaaa-aaaak-aaioq-cai" 13 | } 14 | } -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "canisters": { 3 | "TokenA": { 4 | "main": "./examples/token/Token.mo", 5 | "type": "motoko", 6 | "args" : "--compacting-gc" 7 | }, 8 | "TokenB": { 9 | "main": "./examples/token/Token.mo", 10 | "type": "motoko", 11 | "args" : "--compacting-gc" 12 | }, 13 | "Example": { 14 | "main": "./examples/Example.mo", 15 | "type": "motoko", 16 | "args" : "--compacting-gc" 17 | }, 18 | "Example2PC": { 19 | "main": "./examples/Example2PC.mo", 20 | "type": "motoko", 21 | "args" : "--compacting-gc" 22 | } 23 | 24 | }, 25 | "defaults": { 26 | "build": { 27 | "packtool": "vessel sources" 28 | } 29 | }, 30 | "networks": { 31 | "ic": { 32 | "providers": ["https://ic0.app"], 33 | "type": "persistent" 34 | }, 35 | "local": { 36 | "bind": "0.0.0.0:8000", 37 | "type": "ephemeral" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/TATypes.mo: -------------------------------------------------------------------------------- 1 | import CallType "./CallType"; 2 | import Time "mo:base/Time"; 3 | module { 4 | public type Status = CallType.Status; 5 | public type CallType = CallType.CallType; 6 | public type Receipt = CallType.Receipt; 7 | public type CustomCall = CallType.CustomCall; 8 | public type TaskResult = CallType.TaskResult; 9 | public type Callee = Principal; 10 | public type CalleeStatus = { 11 | successCount: Nat; 12 | failureCount: Nat; 13 | continuousFailure: Nat; 14 | }; 15 | public type Ttid = Nat; // from 1 16 | public type Toid = Nat; // from 1 17 | public type Attempts = Nat; 18 | public type Task = { 19 | callee: Callee; 20 | callType: CallType; 21 | preTtid: [Ttid]; 22 | toid: ?Toid; 23 | forTtid: ?Ttid; 24 | attemptsMax: Attempts; 25 | recallInterval: Int; // nanoseconds 26 | cycles: Nat; 27 | data: ?Blob; 28 | time: Time.Time; 29 | }; 30 | public type AgentCallback = (_ttid: Ttid, _task: Task, _result: TaskResult) -> async* (); 31 | public type TaskCallback = (_toName: Text, _ttid: Ttid, _task: Task, _result: TaskResult) -> async (); 32 | public type TaskEvent = { 33 | toid: ?Toid; 34 | ttid: Ttid; 35 | task: Task; 36 | attempts: Attempts; 37 | result: TaskResult; // (Status, ?Receipt, ?Err) 38 | callbackStatus: ?Status; 39 | time: Time.Time; 40 | txHash: Blob; 41 | }; 42 | public type ErrorLog = { // errorLog 43 | ttid: Ttid; 44 | callee: ?Callee; 45 | result: ?TaskResult; 46 | time: Time.Time; 47 | }; 48 | }; -------------------------------------------------------------------------------- /package-set.dhall: -------------------------------------------------------------------------------- 1 | -- let upstream = https://github.com/dfinity/vessel-package-set/releases/download/mo-0.11.1-20240411/package-set.dhall 2 | -- mo-0.11.1-20240411 3 | let upstream = 4 | [ { dependencies = [] : List Text 5 | , name = "base" 6 | , repo = "https://github.com/dfinity/motoko-base.git" 7 | , version = "6f49e5f877742b79e97ef1b6f226a7f905ba795c" 8 | } 9 | , { dependencies = [ "base" ] 10 | , name = "crud" 11 | , repo = "https://github.com/matthewhammer/motoko-crud.git" 12 | , version = "0367a9a40eb708772df0662232a842c273a263e9" 13 | } 14 | , { dependencies = [ "base" ] 15 | , name = "matchers" 16 | , repo = "https://github.com/kritzcreek/motoko-matchers.git" 17 | , version = "3dac8a071b69e4e651b25a7d9683fe831eb7cffd" 18 | } 19 | , { dependencies = [ "base" ] 20 | , name = "parsec" 21 | , repo = "https://github.com/crusso/mo-parsec.git" 22 | , version = "6d84fe23245dac4c8c6c83f83349d972dd98289c" 23 | } 24 | , { dependencies = [ "base" ] 25 | , name = "scc" 26 | , repo = "https://github.com/nomeata/motoko-scc.git" 27 | , version = "a6ddd4e688f75443674ad8ed24495fbeb103fc7b" 28 | } 29 | , { dependencies = [ "base" ] 30 | , name = "sha256" 31 | , repo = "https://github.com/enzoh/motoko-sha.git" 32 | , version = "9e2468f51ef060ae04fde8d573183191bda30189" 33 | } 34 | , { dependencies = [ "base" ] 35 | , name = "icip" 36 | , repo = "https://github.com/feliciss/icip.git" 37 | , version = "3188b01d7ec5ef354c773c66197869cdce18c4b7" 38 | } 39 | , { dependencies = [ "base" ] 40 | , name = "pretty" 41 | , repo = "https://github.com/kritzcreek/motoko-pretty.git" 42 | , version = "73b81a2df1058396f0395d2d4a38ddcca4531142" 43 | } 44 | , { dependencies = [ "base" ] 45 | , name = "sha224" 46 | , repo = "https://github.com/flyq/motoko-sha224.git" 47 | , version = "82e0aa1a77a8c0a2f98332b59ffc242d820e62cb" 48 | } 49 | , { dependencies = [ "base" ] 50 | , name = "splay" 51 | , repo = "https://github.com/chenyan2002/motoko-splay.git" 52 | , version = "f8e50749f9c4d7ccae99694c35310ac68945a225" 53 | } 54 | , { dependencies = [ "base" ] 55 | , name = "sequence" 56 | , repo = "https://github.com/matthewhammer/motoko-sequence.git" 57 | , version = "e57b88cf4aa4852c7f66b9150692e256911c1425" 58 | } 59 | , { dependencies = [ "base" ] 60 | , name = "base32" 61 | , repo = "https://github.com/flyq/motoko-base32.git" 62 | , version = "067ac54e288f4cd7302be1f400bcddcce70f7d77" 63 | } 64 | , { dependencies = [ "base" ] 65 | , name = "adapton" 66 | , repo = "https://github.com/matthewhammer/motoko-adapton.git" 67 | , version = "d1b130fd930d8b498b66d2a2c6a45beea3f67c3a" 68 | } 69 | , { dependencies = [ "base" ] 70 | , name = "iterext" 71 | , repo = "https://github.com/timohanke/motoko-iterext" 72 | , version = "6c05ff069e00eabdd2fc700d7457878d72dafaa9" 73 | } 74 | , { dependencies = [ "base", "iterext" ] 75 | , name = "sha2" 76 | , repo = "https://github.com/timohanke/motoko-sha2" 77 | , version = "837682e5a503f200f6829081e41dd6c98b8d6bf9" 78 | } 79 | , { dependencies = [ "base" ] 80 | , name = "easy-random" 81 | , repo = "https://github.com/neokree/easy-random.git" 82 | , version = "87acbe9565cb7fb4397c97d871bc8624a6bfb882" 83 | } 84 | , { dependencies = [ "base" ] 85 | , name = "xtended-numbers" 86 | , repo = "https://github.com/edjcase/motoko_numbers" 87 | , version = "773953141f976ccdfc2e6a2c451b841a53bb39a0" 88 | } 89 | , { dependencies = [ "xtended-numbers" ] 90 | , name = "cbor" 91 | , repo = "https://github.com/gekctek/motoko_cbor" 92 | , version = "5adf22b177d187d076b222e94b3fdf071b7c0b65" 93 | } 94 | , { dependencies = [ "base", "sha256", "cbor", "sha224" ] 95 | , name = "ic-certification" 96 | , repo = "https://github.com/nomeata/ic-certification" 97 | , version = "5556c18ab4e9f751affbebbd60508791a102b57f" 98 | } 99 | ] 100 | let Package = 101 | { name : Text, version : Text, repo : Text, dependencies : List Text } 102 | 103 | let 104 | -- This is where you can add your own packages to the package-set 105 | additions = 106 | [ 107 | { dependencies = [ "base", "iterext" ] 108 | , name = "sha3" 109 | , repo = "https://github.com/hanbu97/motoko-sha3" 110 | , version = "db66d478f22720d98cb00fecd8da58533c64f7b4" 111 | } 112 | , { dependencies = [ "base", "sha2" ] 113 | , name = "ecdsa" 114 | , repo = "https://github.com/herumi/ecdsa-motoko" 115 | , version = "63466478d3d2c3dab8481f62d8256a0e75f7cd8b" 116 | } 117 | , { dependencies = [] : List Text 118 | , name = "base-0.7.3" 119 | , repo = "https://github.com/dfinity/motoko-base" 120 | , version = "aafcdee0c8328087aeed506e64aa2ff4ed329b47" 121 | } 122 | , { dependencies = [ "base-0.7.3" ] : List Text 123 | , name = "parser-combinators" 124 | , repo = "https://github.com/aviate-labs/parser-combinators.mo" 125 | , version = "v0.1.2" 126 | } 127 | , { dependencies = [ "base-0.7.3", "parser-combinators" ] 128 | , name = "json" 129 | , repo = "https://github.com/aviate-labs/json.mo" 130 | , version = "afd30ed75095cb339c1ec187d61030bbd2e59ae6" 131 | } 132 | , { dependencies = [ "base", "sha224" ] 133 | , name = "icl" 134 | , repo = "https://github.com/iclighthouse/icl-vessel" 135 | , version = "84a76139068fb9ab81a9cacc713d8d80ed666c58" 136 | } 137 | ] : List Package 138 | 139 | let 140 | {- This is where you can override existing packages in the package-set 141 | 142 | For example, if you wanted to use version `v2.0.0` of the foo library: 143 | let overrides = [ 144 | { name = "foo" 145 | , version = "v2.0.0" 146 | , repo = "https://github.com/bar/foo" 147 | , dependencies = [] : List Text 148 | } 149 | ] 150 | -} 151 | overrides = 152 | [] : List Package 153 | 154 | in upstream # additions # overrides -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # ICTC 2 | 3 | ## 概述 4 | 5 | IC事务协调器(ICTC)是一个用于IC网络上Defi应用开发的分布式事务框架,支持Motoko开发语言。ICTC的核心思想是受到金融领域普遍使用的DTC (Distributed Transaction Coordinator)的启发. 6 | 7 | ## 解决什么问题 8 | 9 | 基于IC网络开发Dapp,特别是Defi应用,我们不得不面对原子性问题,这本质是一个分布式事务的数据一致性问题。与传统分布式事务相比,区块链网络上的分布式事务面临最大的挑战是,对参与主体的信任假设不同。传统分布式事务对参与主体是基于可信假设,而区块链网络上的分布式事务对参与主体是基于不可信假设,它只信任智能合约代码。 10 | 11 | 当Canister的一个方法需要执行跨容器调用,将面临被调用方(Callee)故障而导致数据不一致的威胁。可能出现的三种故障: 12 | 13 | - 业务层错误 (business error): 如Token余额不足。 14 | - 运行时错误 (runtime error): 如除以0错误。 15 | - 子网宕机 (fail-stop): 如被调用方(Callee)容器所在子网不可访问。 16 | 17 | IC网络在大部分时间中不会出现暂时性故障和子网宕机,但仍然存在发生这类故障的的可能性。如果Defi应用不能健壮地处理故障,保持数据最终一致性,后果可能是灾难性的。 18 | 19 | 所以我们需要一个新的框架,需要调用方和被调用方的共同努力,提供有利于分布式事务执行的特性。ICTC是基于[BASE理论](https://queue.acm.org/detail.cfm?id=1394128),在跨容器事务中追求数据最终一致性。但遗憾的是,ICTC只能做到“最大努力交付”,还需要治理补偿或者手工补偿机制作为最后保障,才能实现最终一致性。 20 | 21 | ICTC要求各参与方实现: 22 | 23 | - 被调用方实现内部任务原子性 24 | - 被调用方根据实际情况满足最终一致性的功能,包括: 25 | - 允许重试(要求实现幂等性) 26 | - 使用Nonce(保证交易有序执行,幂等性) 27 | - Txid全局唯一,并可预先计算(txid在发送交易前可得) 28 | - 提供冲正交易函数(回滚操作) 29 | - 调用方采取多种方式实现最终一致性,包括: 30 | - 重试 31 | - 自动冲正 32 | - 治理或者手工冲正 33 | - 先借后贷原则(先收后支,可冻结的先处理) 34 | - 调用方主导原则(由调用方担任协调者) 35 | 36 | 37 | ## 技术架构 38 | 39 | ICTC由事务管理器(Transaction Manager,TM)、任务执行器(Task Actuator,TA)、事务补偿器(Transaction Compensator,TC)组成。 40 | 41 | 事务订单(Transaction Order):指一个具体的事务,包括一个或多个事务任务(Transaction Task),要求所有任务要么全部成功,要么全部拒绝。 42 | 事务任务(Transaction Task):指一个事务中的任务,包括调用者本地任务和其他参与方的远程任务。 43 | 44 | ![ICTC](img/ictc.jpg) 45 | 46 | ## Task Actuator 47 | 48 | Task Actuator使用“最大努力交付”(best-effort delivery)策略,并且肯定会返回结果(成功/失败)。 49 | 50 | ![Task Actuator](img/ictc-ta.png) 51 | 52 | ## Transaction Manager 53 | 54 | Transaction Manager的作用是管理事务状态,操作Actuator,处理异常情况并调用Compensator。Transaction Manager分为Saga事务管理器(Saga Transaction Manager,SagaTM)和2PC事务管理器(2PC Transaction Manager,TPCTM)。 55 | 56 | **Saga Transaction Manager** 57 | 58 | ![Saga Transaction Manager](img/ictc-saga.png) 59 | 60 | **2PC Transaction Manager** 61 | 62 | ![2PC Transaction Manager](img/ictc-2pc.png) 63 | 64 | ## Transaction Compensator 65 | 66 | Transaction Compensator的功能在Transaction Manager中一起实现,其作用是当事务发生异常时执行补偿操作,包括自动补偿、治理或手工补偿。 67 | 68 | ## Roadmap 69 | 70 | - ICTC Framework PoC Version (done) 71 | - Synchronous Actuator (done) 72 | - Saga Transaction Manager (done) 73 | - ICTC Framework Alpha Version (done) 74 | - Transaction Actuator (done) 75 | - 2PC Transaction Manager (done) 76 | - ICTC Framework Beta Version (done) 77 | - ICTC Framework v1.0 (done) 78 | - ICTC Framework v1.5 (done) 79 | - ICTC Framework v2.0 (done) 80 | - ICTC Framework v3.0 (done) 81 | 82 | ## 文档 83 | 84 | [ICTC v3.0 Reference](./docs/ictc_reference-3.0.md) 85 | 86 | [ICTC v2.0 Reference](./docs/ictc_reference-2.0.md) 87 | 88 | [ICTC v1.5 Reference](./docs/ictc_reference-1.5.md) 89 | 90 | [ICTC v1.0 Reference](./docs/ictc_reference.md) 91 | 92 | ## Use in vessel 93 | 94 | package-set.dhall 95 | ``` 96 | ... 97 | { dependencies = [ "base", "icl" ] : List Text 98 | , name = "ictc" 99 | , repo = "https://github.com/iclighthouse/ICTC" 100 | , version = "-- commit hash --" 101 | } 102 | ... 103 | ``` 104 | 105 | vessel.dhall 106 | ``` 107 | { 108 | dependencies = [ "base", "icl", "ictc" ], 109 | compiler = None Text 110 | } 111 | ``` 112 | 113 | motoko 114 | ``` 115 | import CallType "mo:ictc/CallType"; 116 | import TA "mo:ictc/TA"; 117 | import SagaTM "mo:ictc/SagaTM"; 118 | ``` 119 | 120 | ## ICTC explorer 121 | 122 | https://cmqwp-uiaaa-aaaaj-aihzq-cai.raw.ic0.app/ 123 | 124 | ## Examples 125 | 126 | ``` 127 | public shared(msg) func foo(): async (SagaTM.Toid, ?SagaTM.OrderStatus){ 128 | let valueA: Nat = 100000000; // TokenA 129 | let valueB: Nat = 200000000; // TokenB 130 | let tokenFee: Nat = 100000; // TokenA & TokenB 131 | let owner: Text = Principal.toText(msg.caller); 132 | let to: Text = "xxxxx(principal)xxxxx"; 133 | let contract: Text = Principal.toText(Principal.fromActor(this)); 134 | 135 | // Create a Saga transaction order (TO). 136 | let oid = _getSaga().create("swap", #Forward, null, null); 137 | 138 | // Push tasks into TO. 139 | var task = _buildTask(null, tokenA_canister, #DRC20(#drc20_transferFrom(owner, contract, valueA+tokenFee, null, null, null)), []); 140 | let _tid1 =_getSaga().push(oid, task, null, null); 141 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_transferFrom(to, contract, valueB+tokenFee, null, null, null)), []); 142 | let _tid2 =_getSaga().push(oid, task, null, null); 143 | task := _buildTask(null, Principal.fromActor(this), #custom(#This(#foo(1))), []); 144 | let _tid3 =_getSaga().push(oid, task, null, null); 145 | task := _buildTask(null, tokenA_canister, #DRC20(#drc20_transfer(to, valueA, null, null, null)), []); 146 | let _tid4 =_getSaga().push(oid, task, null, null); 147 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_transfer(owner, valueB, null, null, null)), []); 148 | let _tid5 =_getSaga().push(oid, task, null, null); 149 | 150 | // After this, no further push tasks are allowed. 151 | _getSaga().close(oid); 152 | 153 | // Execute the transaction order (TO), where all tasks are executed sequentially and synchronously, and if one of the tasks is failed, the whole TO will be suspended and marked as "Blocking". 154 | let res = await _getSaga().run(oid); 155 | 156 | return (oid, res); 157 | }; 158 | ``` 159 | 160 | ### Example.mo (Saga) 161 | 162 | [./examples/Example.mo](./examples/Example.mo) 163 | 164 | ### Example2PC.mo (2PC) 165 | 166 | [./examples/Example2PC.mo](./examples/Example2PC.mo) 167 | 168 | ## 正在采用的项目 169 | 170 | - [ICDex](http://icdex.io) (Orderbook DEX) 171 | - [ICSwap](http://icswap.io) (AMM DEX) 172 | 173 | ## Community 174 | 175 | Twitter: [@ICLighthouse](https://twitter.com/ICLighthouse) 176 | Medium: [https://medium.com/@ICLighthouse](https://medium.com/@ICLighthouse) 177 | Discord: [https://discord.gg/FQZFGGq7zv](https://discord.gg/FQZFGGq7zv) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ICTC 2 | 3 | ## Abstract 4 | 5 | IC Transaction Coordinator (ICTC) is a distributed transaction framework for Defi applications on IC network. It supports Motoko language. The core idea of ICTC is inspired by the DTC (Distributed Transaction Coordinator), which is commonly used in the financial sector. 6 | 7 | ## Motivation 8 | 9 | Developing dapps based on IC networks, especially Defi applications, we have to face the problem of atomicity, which is essentially a data consistency problem for distributed transactions. The biggest challenge facing distributed transactions on blockchain networks compared to traditional distributed transactions is the different assumption of trust in the participating entities. While traditional distributed transactions are based on trustworthiness assumptions for participating entities, distributed transactions on blockchain networks are based on untrustworthiness assumptions for participating entities, and it only trusts smart contract code. 10 | 11 | When a method in Canister needs to execute a cross-container call, it will face the threat of inconsistent data due to a fault in the callee. Three types of faults may occur. 12 | 13 | - Business error: e.g. insufficient token balance. 14 | - Runtime error: e.g. divide-by-0 error. 15 | - Fail-stop: e.g. the subnet where the callee is located is inaccessible. 16 | 17 | The IC network is free from fail-stop most of the time, but there is still the potential for this type of failures to occur. If the Defi application cannot robustly handle failures and maintain eventual consistency, the consequences could be catastrophic. 18 | 19 | So we need a new framework that requires a concerted effort from both the caller and the callee to provide features that support the execution of distributed transactions. ICTC is based on [Base: An Acid Alternative](https://queue.acm.org/detail.cfm?id=1394128), and seeks eventual consistency of data in cross-canister transactions. Unfortunately, ICTC can only achieve "best-effort delivery" and requires governance compensation or manual compensation mechanisms as a final guarantee to achieve eventual consistency. 20 | 21 | ICTC requires that each participant achieves. 22 | 23 | - The callee achieves internal task atomicity 24 | - The callee implements functionality to meet eventual consistency as appropriate, including 25 | - Allowing retries (requires idempotency) 26 | - Use Nonce (guarantees orderly execution of txns, idempotency) 27 | - Txid is globally unique and can be pre-calculated (txid can be calculated before sending a txn) 28 | - Provision of reversal transaction functions (rollback operations) 29 | - Caller takes a variety of ways to achieve eventual consistency, including 30 | - Retries 31 | - Automatic reversal transaction 32 | - Governance or manual reversal transaction 33 | - Debit first, credit later principle (receive first, freezable txn first) 34 | - Caller-led principle (the caller acts as coordinator) 35 | 36 | 37 | ## Technology Architecture 38 | 39 | ICTC consists of Transaction Manager (TM), Task Actuator (TA), and Transaction Compensator (TC). 40 | 41 | Transaction Order: A transaction, including one or more transaction tasks, that requires all tasks to be either fully successful or fully rejected. 42 | Transaction Task: A task within a transaction, including local tasks of the caller and remote tasks of other parties involved. 43 | 44 | ![ICTC](img/ictc.jpg) 45 | 46 | ## Task Actuator 47 | 48 | The Task Actuator uses a "best-effort delivery" policy and will definitely return a result (success/failure). 49 | 50 | ![Task Actuator](img/ictc-ta.png) 51 | 52 | ## Transaction Manager 53 | 54 | The Transaction Manager is used to manage the status of transactions, operate the Actuator, handle exceptions and call the Compensator. Transaction Manager implementation includes the Saga Transaction Manager (SagaTM) and the 2PC Transaction Manager (TPCTM). 55 | 56 | **Saga Transaction Manager** 57 | 58 | ![Saga Transaction Manager](img/ictc-saga.png) 59 | 60 | **2PC Transaction Manager** 61 | 62 | ![2PC Transaction Manager](img/ictc-2pc.png) 63 | 64 | ## Transaction Compensator 65 | 66 | The Transaction Compensator function is implemented in Transaction Manager and is used to execute compensation operations when an exception occurs in a transaction, including automatic compensation, governance or manual compensation. 67 | 68 | ## Roadmap 69 | 70 | - ICTC Framework PoC Version (done) 71 | - Synchronous Actuator (done) 72 | - Saga Transaction Manager (done) 73 | - ICTC Framework Alpha Version (done) 74 | - Transaction Actuator (done) 75 | - 2PC Transaction Manager (done) 76 | - ICTC Framework Beta Version (done) 77 | - ICTC Framework v1.0 (done) 78 | - ICTC Framework v1.5 (done) 79 | - ICTC Framework v2.0 (done) 80 | - ICTC Framework v3.0 (done) 81 | 82 | ## Reference Docs 83 | 84 | Note: Version 3.0 is not compatible with version 2.0 and requires refactoring your code to upgrade. 85 | 86 | [ICTC v3.0 Reference](./docs/ictc_reference-3.0.md) 87 | 88 | [ICTC v2.0 Reference](./docs/ictc_reference-2.0.md) 89 | 90 | [ICTC v1.5 Reference](./docs/ictc_reference-1.5.md) 91 | 92 | [ICTC v1.0 Reference](./docs/ictc_reference.md) 93 | 94 | ## Use in vessel 95 | 96 | package-set.dhall 97 | ``` 98 | ... 99 | { dependencies = [ "base", "icl" ] : List Text 100 | , name = "ictc" 101 | , repo = "https://github.com/iclighthouse/ICTC" 102 | , version = "-- commit hash --" 103 | } 104 | ... 105 | ``` 106 | 107 | vessel.dhall 108 | ``` 109 | { 110 | dependencies = [ "base", "icl", "ictc" ], 111 | compiler = None Text 112 | } 113 | ``` 114 | 115 | motoko 116 | ``` 117 | import CallType "mo:ictc/CallType"; 118 | import TA "mo:ictc/TA"; 119 | import SagaTM "mo:ictc/SagaTM"; 120 | ``` 121 | 122 | ## ICTC explorer 123 | 124 | https://cmqwp-uiaaa-aaaaj-aihzq-cai.raw.ic0.app/ 125 | 126 | ## Examples 127 | 128 | ``` 129 | public shared(msg) func foo(): async (SagaTM.Toid, ?SagaTM.OrderStatus){ 130 | let valueA: Nat = 100000000; // TokenA 131 | let valueB: Nat = 200000000; // TokenB 132 | let tokenFee: Nat = 100000; // TokenA & TokenB 133 | let owner: Text = Principal.toText(msg.caller); 134 | let to: Text = "xxxxx(principal)xxxxx"; 135 | let contract: Text = Principal.toText(Principal.fromActor(this)); 136 | 137 | // Create a Saga transaction order (TO). 138 | let oid = _getSaga().create("swap", #Forward, null, null); 139 | 140 | // Push tasks into TO. 141 | var task = _buildTask(null, tokenA_canister, #DRC20(#drc20_transferFrom(owner, contract, valueA+tokenFee, null, null, null)), []); 142 | let _tid1 =_getSaga().push(oid, task, null, null); 143 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_transferFrom(to, contract, valueB+tokenFee, null, null, null)), []); 144 | let _tid2 =_getSaga().push(oid, task, null, null); 145 | task := _buildTask(null, Principal.fromActor(this), #custom(#This(#foo(1))), []); 146 | let _tid3 =_getSaga().push(oid, task, null, null); 147 | task := _buildTask(null, tokenA_canister, #DRC20(#drc20_transfer(to, valueA, null, null, null)), []); 148 | let _tid4 =_getSaga().push(oid, task, null, null); 149 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_transfer(owner, valueB, null, null, null)), []); 150 | let _tid5 =_getSaga().push(oid, task, null, null); 151 | 152 | // After this, no further push tasks are allowed. 153 | _getSaga().close(oid); 154 | 155 | // Execute the transaction order (TO), where all tasks are executed sequentially and synchronously, and if one of the tasks is failed, the whole TO will be suspended and marked as "Blocking". 156 | let res = await _getSaga().run(oid); 157 | 158 | return (oid, res); 159 | }; 160 | ``` 161 | 162 | ### Example.mo (Saga) 163 | 164 | [./examples/Example.mo](./examples/Example.mo) 165 | 166 | ### Example2PC.mo (2PC) 167 | 168 | [./examples/Example2PC.mo](./examples/Example2PC.mo) 169 | 170 | ## Powering projects 171 | 172 | - [ICDex](http://icdex.io) (Orderbook DEX) 173 | - [ICSwap](http://icswap.io) (AMM DEX) 174 | 175 | ## Community 176 | 177 | Twitter: [@ICLighthouse](https://twitter.com/ICLighthouse) 178 | Medium: [https://medium.com/@ICLighthouse](https://medium.com/@ICLighthouse) 179 | Discord: [https://discord.gg/FQZFGGq7zv](https://discord.gg/FQZFGGq7zv) -------------------------------------------------------------------------------- /docs/ictc_reference.md: -------------------------------------------------------------------------------- 1 | # ICTC Reference 2 | 3 | Latest: [ICTC v1.5 Reference](ictc_reference-1.5.md) 4 | 5 | ## Quickstart 6 | 7 | ### ICTC(Saga) - Common implementations 8 | 9 | - Imports SagaTM Module. 10 | ``` 11 | import SagaTM "./src/SagaTM"; 12 | ``` 13 | 14 | - (Optional) Implements callback functions for transaction orders and transaction tasks. 15 | ``` 16 | private func _taskCallback(_tid: SagaTM.Ttid, _task: SagaTM.Task, _result: SagaTM.TaskResult) : async (){ 17 | // do something 18 | }; 19 | private func _orderCallback(_oid: SagaTM.Toid, _status: SagaTM.OrderStatus, _data: ?Blob) : async (){ 20 | // do something 21 | }; 22 | ``` 23 | 24 | - Implements local tasks. (Each task needs to be internally atomic or able to maintain data consistency.) 25 | ``` 26 | private func _local(_args: SagaTM.CallType, _receipt: ?SagaTM.Receipt) : async (SagaTM.TaskResult){ 27 | switch(_args){ 28 | case(#This(method)){ 29 | // switch(method){ 30 | // case(#local_method_name(args)){ 31 | // // ... 32 | // }; 33 | // }; 34 | }; 35 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 36 | }; 37 | }; 38 | ``` 39 | Example: 40 | ``` 41 | private var x : Nat = 0; 42 | private func foo(count: Nat) : (){ 43 | x += 100; 44 | }; 45 | private func _local(_args: SagaTM.CallType, _receipt: ?SagaTM.Receipt) : async (SagaTM.TaskResult){ 46 | switch(_args){ 47 | case(#This(method)){ 48 | switch(method){ 49 | case(#foo(count)){ 50 | var result = foo(count); // Receipt 51 | return (#Done, ?#This(#foo(result)), null); 52 | }; 53 | }; 54 | }; 55 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 56 | }; 57 | }; 58 | ``` 59 | 60 | - (Optional) Custom CallType.mo file. 61 | Modify the CallType.mo file according to your local tasks and the external tasks you need to call. 62 | 63 | - Creates a saga object. 64 | ``` 65 | let saga = SagaTM.SagaTM(Principal.fromActor(this), _local, ?_taskCallback, ?_orderCallback); 66 | ``` 67 | 68 | - Creates a transaction order. (Supports `Forward` and `Backward` modes) 69 | ``` 70 | let oid = saga.create("TO_name", #Forward, null, null); 71 | ``` 72 | 73 | - Pushs one or more transaction tasks to the specified transaction order. 74 | ``` 75 | let task: SagaTM.PushTaskRequest = { // for example 76 | callee = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai"); 77 | callType = #Ledger(#transfer(ledger_transferArgs)); // method to be called 78 | preTtid = []; // Pre-dependent tasks 79 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 80 | recallInterval = ?0; // nanoseconds 81 | cycles = 0; 82 | data = null; 83 | }; 84 | let tid1 = saga.push(oid, task, null, null); 85 | ``` 86 | 87 | - Executes the transaction order. 88 | ``` 89 | saga.finish(oid); 90 | let res = await saga.run(oid); 91 | ``` 92 | 93 | - Implements the upgrade function for SagaTM. 94 | ``` 95 | private stable var __sagaData: [SagaTM.Data] = []; 96 | system func preupgrade() { 97 | __sagaData := TA.arrayAppend(__sagaData, [_getSaga().getData()]); 98 | }; 99 | system func postupgrade() { 100 | if (__sagaData.size() > 0){ 101 | _getSaga().setData(__sagaData[0]); 102 | __sagaData := []; 103 | }; 104 | }; 105 | ``` 106 | 107 | ### ICTC(2PC) 108 | 109 | - Imports TPCTM Module. 110 | ``` 111 | import TPCTM "./src/TPCTM"; 112 | ``` 113 | 114 | - (Optional) Implements callback functions for transaction orders and transaction tasks. 115 | ``` 116 | private func _taskCallback(_tid: TPCTM.Ttid, _task: TPCTM.Task, _result: TPCTM.TaskResult) : async (){ 117 | // do something 118 | }; 119 | private func _orderCallback(_oid: TPCTM.Toid, _status: TPCTM.OrderStatus, _data: ?Blob) : async (){ 120 | // do something 121 | }; 122 | ``` 123 | 124 | - Implements local tasks. (Each task needs to be internally atomic or able to maintain data consistency.) 125 | ``` 126 | private func _local(_args: TPCTM.CallType, _receipt: ?TPCTM.Receipt) : async (TPCTM.TaskResult){ 127 | switch(_args){ 128 | case(#This(method)){ 129 | // switch(method){ 130 | // case(#local_method_name(args)){ 131 | // // ... 132 | // }; 133 | // }; 134 | }; 135 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 136 | }; 137 | }; 138 | ``` 139 | Example: 140 | ``` 141 | private var x : Nat = 0; 142 | private func foo(count: Nat) : (){ 143 | x += 100; 144 | }; 145 | private func _local(_args: TPCTM.CallType, _receipt: ?TPCTM.Receipt) : async (TPCTM.TaskResult){ 146 | switch(_args){ 147 | case(#This(method)){ 148 | switch(method){ 149 | case(#foo(count)){ 150 | var result = foo(count); // Receipt 151 | return (#Done, ?#This(#foo(result)), null); 152 | }; 153 | }; 154 | }; 155 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 156 | }; 157 | }; 158 | ``` 159 | 160 | - (Optional) Custom CallType.mo file. 161 | Modify the CallType.mo file according to your local tasks and the external tasks you need to call. 162 | 163 | - Creates a tpc object. 164 | ``` 165 | let tpc = TPCTM.TPCTM(Principal.fromActor(this), _local, ?_taskCallback, ?_orderCallback); 166 | ``` 167 | 168 | - Creates a transaction order. 169 | ``` 170 | let oid = tpc.create("TO_name", null, null); 171 | ``` 172 | 173 | - Pushs one or more transaction tasks to the specified transaction order. 174 | ``` 175 | let prepare: TPCTM.TaskRequest = { // for example 176 | callee = token_canister; 177 | callType = #DRC20(#lockTransferFrom(caller, to, value, 5*60, null, null, null, null)); // method to be called 178 | preTtid = []; // Pre-dependent tasks 179 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 180 | recallInterval = ?0; // nanoseconds 181 | cycles = 0; 182 | data = null; 183 | }; 184 | let commit: TPCTM.TaskRequest = { // for example 185 | callee = token_canister; 186 | callType = #DRC20(#executeTransfer(#AutoFill, #sendAll, null, null, null, null)); // method to be called 187 | preTtid = []; // Pre-dependent tasks 188 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 189 | recallInterval = ?0; // nanoseconds 190 | cycles = 0; 191 | data = null; 192 | }; 193 | let tid1 = tpc.push(oid, prepare, commit, null, null, null); 194 | ``` 195 | 196 | - Executes the transaction order. 197 | ``` 198 | tpc.finish(oid); 199 | let res = await tpc.run(oid); 200 | ``` 201 | 202 | - Implements the upgrade function for TPCTM. 203 | ``` 204 | private stable var __tpcData: [TPCTM.Data] = []; 205 | system func preupgrade() { 206 | __tpcData := TA.arrayAppend(__tpcData, [_getTPC().getData()]); 207 | }; 208 | system func postupgrade() { 209 | if (__tpcData.size() > 0){ 210 | _getTPC().setData(__tpcData[0]); 211 | __tpcData := []; 212 | }; 213 | }; 214 | ``` 215 | 216 | ## Methods (API) 217 | 218 | ### ICTC(Saga) 219 | 220 | ``` 221 | public func create (_name: Text, _compStrategy: CompStrategy, _data: ?Blob, _callback: ?OrderCallback) : Toid 222 | 223 | public func push(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 224 | 225 | public func open(_toid: Toid) : () 226 | 227 | public func finish(_toid: Toid) : () 228 | 229 | public func run(_toid: Toid) : async ?OrderStatus 230 | 231 | public func count() : Nat 232 | 233 | public func status(_toid: Toid) : ?OrderStatus 234 | 235 | public func isCompleted(_toid: Toid) : Bool 236 | 237 | public func isTaskCompleted(_ttid: Ttid) : Bool 238 | 239 | public func getOrder(_toid: Toid) : ?Order 240 | 241 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat} 242 | 243 | public func getAliveOrders() : [(Toid, ?Order)] 244 | 245 | public func getTaskEvents(_toid: Toid) : [TaskEvent] 246 | 247 | public func getActuator() : TA.TA 248 | 249 | public func setCacheExpiration(_expiration: Int) : () 250 | 251 | public func clear(_delExc: Bool) : () 252 | 253 | public func update(_toid: Toid, _ttid: Ttid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 254 | 255 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid 256 | 257 | public func append(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 258 | 259 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: PushCompRequest, _callback: ?Callback) : Tcid 260 | 261 | public func complete(_toid: Toid, _status: OrderStatus) : async Bool 262 | 263 | public func getData() : Data 264 | 265 | public func setData(_data: Data) : () 266 | ``` 267 | 268 | ### ICTC(2PC) 269 | 270 | ``` 271 | public func create(_name: Text, _data: ?Blob, _callback: ?OrderCallback) : Toid 272 | 273 | public func push(_toid: Toid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 274 | 275 | public func open(_toid: Toid) : () 276 | 277 | public func finish(_toid: Toid) : () 278 | 279 | public func run(_toid: Toid) : async ?OrderStatus 280 | 281 | public func count() : Nat 282 | 283 | public func status(_toid: Toid) : ?OrderStatus 284 | 285 | public func isCompleted(_toid: Toid) : Bool 286 | 287 | public func isTaskCompleted(_ttid: Ttid) : Bool 288 | 289 | public func getOrder(_toid: Toid) : ?Order 290 | 291 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat} 292 | 293 | public func getAliveOrders() : [(Toid, ?Order)] 294 | 295 | public func getTaskEvents(_toid: Toid) : [TaskEvent] 296 | 297 | public func getActuator() : TA.TA 298 | 299 | public func setCacheExpiration(_expiration: Int) : () 300 | 301 | public func clear(_delExc: Bool) : () 302 | 303 | public func update(_toid: Toid, _ttid: Ttid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 304 | 305 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid 306 | 307 | public func append(_toid: Toid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 308 | 309 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: TaskRequest, _callback: ?Callback) : Tcid 310 | 311 | public func complete(_toid: Toid, _status: OrderStatus) : async Bool 312 | 313 | public func getData() : Data 314 | 315 | public func setData(_data: Data) : () 316 | ``` -------------------------------------------------------------------------------- /docs/ictc_reference-2.0.md: -------------------------------------------------------------------------------- 1 | # ICTC Reference 2 | 3 | ## Quickstart 4 | 5 | ### ICTC v2.0 (Saga) - Common implementations 6 | 7 | - Imports SagaTM Module. 8 | ``` 9 | import SagaTM "./src/SagaTM"; 10 | ``` 11 | 12 | - (Optional) Implements callback functions for transaction orders and transaction tasks. 13 | ``` 14 | private func _taskCallback(_name: Text, _tid: SagaTM.Ttid, _task: SagaTM.Task, _result: SagaTM.TaskResult) : (){ 15 | // do something 16 | }; 17 | private func _orderCallback(_name: Text, _oid: SagaTM.Toid, _status: SagaTM.OrderStatus, _data: ?Blob) : (){ 18 | // do something 19 | }; 20 | ``` 21 | 22 | - Implements local tasks. (Each task needs to be internally atomic or able to maintain data consistency.) 23 | ``` 24 | private func _local(_args: SagaTM.CallType, _receipt: ?SagaTM.Receipt) : async (SagaTM.TaskResult){ 25 | switch(_args){ 26 | case(#This(method)){ 27 | // switch(method){ 28 | // case(#local_method_name(args)){ 29 | // // ... 30 | // }; 31 | // }; 32 | }; 33 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 34 | }; 35 | }; 36 | ``` 37 | 38 | - (Optional) Custom CallType.mo file. 39 | Modify the CallType.mo file according to your local tasks and the external tasks you need to call. 40 | 41 | - Creates a saga object. 42 | ``` 43 | let saga = SagaTM.SagaTM(Principal.fromActor(this), ?_local, ?_taskCallback, ?_orderCallback); 44 | ``` 45 | 46 | - Creates a transaction order. (Supports `Forward` and `Backward` modes) 47 | ``` 48 | let oid = saga.create("TO_name", #Forward, null, null); 49 | ``` 50 | 51 | - Pushs one or more transaction tasks to the specified transaction order. 52 | ``` 53 | let task: SagaTM.PushTaskRequest = { // for example 54 | callee = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai"); 55 | callType = #Ledger(#transfer(ledger_transferArgs)); // method to be called 56 | preTtid = []; // Pre-dependent tasks 57 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 58 | recallInterval = ?0; // nanoseconds 59 | cycles = 0; 60 | data = null; 61 | }; 62 | let tid1 = saga.push(oid, task, null, null); 63 | ``` 64 | 65 | - Executes the transaction order. 66 | ``` 67 | saga.close(oid); 68 | let res = await saga.run(oid); 69 | ``` 70 | 71 | - Implements the upgrade function for SagaTM. 72 | ``` 73 | private stable var __sagaDataNew: ?SagaTM.Data = null; 74 | system func preupgrade() { 75 | let data = _getSaga().getData(); 76 | __sagaDataNew := ?data; 77 | // assert(List.size(data.actuator.tasks.0) == 0 and List.size(data.actuator.tasks.1) == 0); 78 | }; 79 | system func postupgrade() { 80 | switch(__sagaDataNew){ 81 | case(?(data)){ 82 | _getSaga().setData(data); 83 | __sagaDataNew := null; 84 | }; 85 | case(_){}; 86 | }; 87 | }; 88 | ``` 89 | 90 | ### ICTC v2.0 (2PC) 91 | 92 | - Imports TPCTM Module. 93 | ``` 94 | import TPCTM "./src/TPCTM"; 95 | ``` 96 | 97 | - (Optional) Implements callback functions for transaction orders and transaction tasks. 98 | ``` 99 | private func _taskCallback(_name: Text, _tid: TPCTM.Ttid, _task: TPCTM.Task, _result: TPCTM.TaskResult) : (){ 100 | // do something 101 | }; 102 | private func _orderCallback(_name: Text, _oid: TPCTM.Toid, _status: TPCTM.OrderStatus, _data: ?Blob) : (){ 103 | // do something 104 | }; 105 | ``` 106 | 107 | - Implements local tasks. (Each task needs to be internally atomic or able to maintain data consistency.) 108 | ``` 109 | private func _local(_args: TPCTM.CallType, _receipt: ?TPCTM.Receipt) : async (TPCTM.TaskResult){ 110 | switch(_args){ 111 | case(#This(method)){ 112 | // switch(method){ 113 | // case(#local_method_name(args)){ 114 | // // ... 115 | // }; 116 | // }; 117 | }; 118 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 119 | }; 120 | }; 121 | ``` 122 | 123 | - (Optional) Custom CallType.mo file. 124 | Modify the CallType.mo file according to your local tasks and the external tasks you need to call. 125 | 126 | - Creates a tpc object. 127 | ``` 128 | let tpc = TPCTM.TPCTM(Principal.fromActor(this), ?_local, ?_taskCallback, ?_orderCallback); 129 | ``` 130 | 131 | - Creates a transaction order. 132 | ``` 133 | let oid = tpc.create("TO_name", null, null); 134 | ``` 135 | 136 | - Pushs one or more transaction tasks to the specified transaction order. 137 | ``` 138 | let prepare: TPCTM.TaskRequest = { // for example 139 | callee = token_canister; 140 | callType = #DRC20(#lockTransferFrom(caller, to, value, 5*60, null, null, null, null)); // method to be called 141 | preTtid = []; // Pre-dependent tasks 142 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 143 | recallInterval = ?0; // nanoseconds 144 | cycles = 0; 145 | data = null; 146 | }; 147 | let commit: TPCTM.TaskRequest = { // for example 148 | callee = token_canister; 149 | callType = #DRC20(#executeTransfer(#AutoFill, #sendAll, null, null, null, null)); // method to be called 150 | preTtid = []; // Pre-dependent tasks 151 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 152 | recallInterval = ?0; // nanoseconds 153 | cycles = 0; 154 | data = null; 155 | }; 156 | let tid1 = tpc.push(oid, prepare, commit, null, null, null); 157 | ``` 158 | 159 | - Executes the transaction order. 160 | ``` 161 | tpc.close(oid); 162 | let res = await tpc.run(oid); 163 | ``` 164 | 165 | - Implements the upgrade function for TPCTM. 166 | ``` 167 | private stable var __tpcDataNew: ?TPCTM.Data = null; 168 | system func preupgrade() { 169 | let data = _getTPC().getData(); 170 | __tpcDataNew := ?data; 171 | // assert(List.size(data.actuator.tasks.0) == 0 and List.size(data.actuator.tasks.1) == 0); 172 | }; 173 | system func postupgrade() { 174 | switch(__tpcDataNew){ 175 | case(?(data)){ 176 | _getTPC().setData(data); 177 | __tpcDataNew := null; 178 | }; 179 | case(_){}; 180 | }; 181 | }; 182 | ``` 183 | 184 | ## Methods (API) 185 | 186 | ### ICTC(Saga) 187 | 188 | ``` 189 | public func create(_name: Text, _compStrategy: CompStrategy, _data: ?Blob, _callback: ?OrderCallback) : Toid 190 | 191 | public func push(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 192 | 193 | public func taskDone(_toid: Toid, _ttid: Ttid, _toCallback: Bool) : async* ?Ttid 194 | 195 | public func redo(_toid: Toid, _ttid: Ttid) : ?Ttid 196 | 197 | public func open(_toid: Toid) : () 198 | 199 | public func close(_toid: Toid) : () 200 | 201 | public func run(_toid: Toid) : async ?OrderStatus 202 | 203 | public func runSync(_toid: Toid) : async ?OrderStatus 204 | 205 | public func asyncMessageSize() : Nat 206 | 207 | public func count() : Nat 208 | 209 | public func status(_toid: Toid) : ?OrderStatus 210 | 211 | public func isCompleted(_toid: Toid) : Bool 212 | 213 | public func isTaskCompleted(_ttid: Ttid) : Bool 214 | 215 | public func getOrder(_toid: Toid) : ?Order 216 | 217 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat} 218 | 219 | public func getAliveOrders() : [(Toid, ?Order)] 220 | 221 | public func getBlockingOrders() : [(Toid, Order)] 222 | 223 | public func getTaskEvents(_toid: Toid) : [TaskEvent] 224 | 225 | public func getActuator() : TA.TA 226 | 227 | public func setCacheExpiration(_expiration: Int) : () 228 | 229 | public func clear(_expiration: ?Int, _delForced: Bool) : () 230 | 231 | public func update(_toid: Toid, _ttid: Ttid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 232 | 233 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid 234 | 235 | public func append(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 236 | 237 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: PushCompRequest, _callback: ?Callback) : Tcid 238 | 239 | public func complete(_toid: Toid, _status: OrderStatus) : async* Bool 240 | 241 | public func doneEmpty(_toid: Toid) : Bool 242 | 243 | public func done(_toid: Toid, _status: OrderStatus, _toCallback: Bool) : async* Bool 244 | 245 | public func block(_toid: Toid) : ?Toid 246 | 247 | public func getData() : Data 248 | 249 | public func setData(_data: Data) : () 250 | ``` 251 | 252 | ### ICTC(2PC) 253 | 254 | ``` 255 | public func create(_name: Text, _data: ?Blob, _callback: ?OrderCallback) : Toid 256 | 257 | public func push(_toid: Toid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 258 | 259 | public func taskDone(_toid: Toid, _ttid: Ttid, _toCallback: Bool) : async* ?Ttid 260 | 261 | public func redo(_toid: Toid, _ttid: Ttid) : ?Ttid 262 | 263 | public func open(_toid: Toid) : () 264 | 265 | public func close(_toid: Toid) : () 266 | 267 | public func run(_toid: Toid) : async ?OrderStatus 268 | 269 | public func runSync(_toid: Toid) : async ?OrderStatus 270 | 271 | public func asyncMessageSize() : Nat 272 | 273 | public func count() : Nat 274 | 275 | public func status(_toid: Toid) : ?OrderStatus 276 | 277 | public func isCompleted(_toid: Toid) : Bool 278 | 279 | public func isTaskCompleted(_ttid: Ttid) : Bool 280 | 281 | public func getOrder(_toid: Toid) : ?Order 282 | 283 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat} 284 | 285 | public func getAliveOrders() : [(Toid, ?Order)] 286 | 287 | public func getBlockingOrders() : [(Toid, Order)] 288 | 289 | public func getTaskEvents(_toid: Toid) : [TaskEvent] 290 | 291 | public func getActuator() : TA.TA 292 | 293 | public func setCacheExpiration(_expiration: Int) : () 294 | 295 | public func clear(_expiration: ?Int, _delForced: Bool) : () 296 | 297 | public func update(_toid: Toid, _ttid: Ttid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 298 | 299 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid 300 | 301 | public func append(_toid: Toid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 302 | 303 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: TaskRequest, _callback: ?Callback) : Tcid 304 | 305 | public func complete(_toid: Toid, _status: OrderStatus) : async* Bool 306 | 307 | public func doneEmpty(_toid: Toid) : Bool 308 | 309 | public func done(_toid: Toid, _status: OrderStatus, _toCallback: Bool) : async* Bool 310 | 311 | public func block(_toid: Toid) : ?Toid 312 | 313 | public func getData() : Data 314 | 315 | public func setData(_data: Data) : () 316 | ``` -------------------------------------------------------------------------------- /docs/ictc_reference-1.5.md: -------------------------------------------------------------------------------- 1 | # ICTC Reference 2 | 3 | ## Quickstart 4 | 5 | ### ICTC v1.5 (Saga) - Common implementations 6 | 7 | - Imports SagaTM Module. 8 | ``` 9 | import SagaTM "./src/SagaTM"; 10 | ``` 11 | 12 | - (Optional) Implements callback functions for transaction orders and transaction tasks. 13 | ``` 14 | private func _taskCallback(_tid: SagaTM.Ttid, _task: SagaTM.Task, _result: SagaTM.TaskResult) : (){ 15 | // do something 16 | }; 17 | private func _orderCallback(_oid: SagaTM.Toid, _status: SagaTM.OrderStatus, _data: ?Blob) : (){ 18 | // do something 19 | }; 20 | ``` 21 | 22 | - Implements local tasks. (Each task needs to be internally atomic or able to maintain data consistency.) 23 | ``` 24 | private func _local(_args: SagaTM.CallType, _receipt: ?SagaTM.Receipt) : (SagaTM.TaskResult){ 25 | switch(_args){ 26 | case(#This(method)){ 27 | // switch(method){ 28 | // case(#local_method_name(args)){ 29 | // // ... 30 | // }; 31 | // }; 32 | }; 33 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 34 | }; 35 | }; 36 | private func _localAsync(_args: SagaTM.CallType, _receipt: ?SagaTM.Receipt) : async (SagaTM.TaskResult){ 37 | switch(_args){ 38 | case(#This(method)){ 39 | // switch(method){ 40 | // case(#local_method_name(args)){ 41 | // // ... 42 | // }; 43 | // }; 44 | }; 45 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 46 | }; 47 | }; 48 | ``` 49 | 50 | - (Optional) Custom CallType.mo file. 51 | Modify the CallType.mo file according to your local tasks and the external tasks you need to call. 52 | 53 | - Creates a saga object. 54 | ``` 55 | let saga = SagaTM.SagaTM(Principal.fromActor(this), ?_local, ?_localAsync, ?_taskCallback, ?_orderCallback); 56 | ``` 57 | 58 | - Creates a transaction order. (Supports `Forward` and `Backward` modes) 59 | ``` 60 | let oid = saga.create("TO_name", #Forward, null, null); 61 | ``` 62 | 63 | - Pushs one or more transaction tasks to the specified transaction order. 64 | ``` 65 | let task: SagaTM.PushTaskRequest = { // for example 66 | callee = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai"); 67 | callType = #Ledger(#transfer(ledger_transferArgs)); // method to be called 68 | preTtid = []; // Pre-dependent tasks 69 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 70 | recallInterval = ?0; // nanoseconds 71 | cycles = 0; 72 | data = null; 73 | }; 74 | let tid1 = saga.push(oid, task, null, null); 75 | ``` 76 | 77 | - Executes the transaction order. 78 | ``` 79 | saga.close(oid); 80 | let res = await saga.run(oid); 81 | ``` 82 | 83 | - Implements the upgrade function for SagaTM. 84 | ``` 85 | private stable var __sagaDataNew: ?SagaTM.Data = null; 86 | system func preupgrade() { 87 | let data = _getSaga().getData(); 88 | __sagaDataNew := ?data; 89 | // assert(List.size(data.actuator.tasks.0) == 0 and List.size(data.actuator.tasks.1) == 0); 90 | }; 91 | system func postupgrade() { 92 | switch(__sagaDataNew){ 93 | case(?(data)){ 94 | _getSaga().setData(data); 95 | __sagaDataNew := null; 96 | }; 97 | case(_){}; 98 | }; 99 | }; 100 | ``` 101 | 102 | ### ICTC v1.5 (2PC) 103 | 104 | - Imports TPCTM Module. 105 | ``` 106 | import TPCTM "./src/TPCTM"; 107 | ``` 108 | 109 | - (Optional) Implements callback functions for transaction orders and transaction tasks. 110 | ``` 111 | private func _taskCallback(_tid: TPCTM.Ttid, _task: TPCTM.Task, _result: TPCTM.TaskResult) : (){ 112 | // do something 113 | }; 114 | private func _orderCallback(_oid: TPCTM.Toid, _status: TPCTM.OrderStatus, _data: ?Blob) : (){ 115 | // do something 116 | }; 117 | ``` 118 | 119 | - Implements local tasks. (Each task needs to be internally atomic or able to maintain data consistency.) 120 | ``` 121 | private func _local(_args: TPCTM.CallType, _receipt: ?TPCTM.Receipt) : (TPCTM.TaskResult){ 122 | switch(_args){ 123 | case(#This(method)){ 124 | // switch(method){ 125 | // case(#local_method_name(args)){ 126 | // // ... 127 | // }; 128 | // }; 129 | }; 130 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 131 | }; 132 | }; 133 | private func _local(_args: TPCTM.CallType, _receipt: ?TPCTM.Receipt) : async (TPCTM.TaskResult){ 134 | switch(_args){ 135 | case(#This(method)){ 136 | // switch(method){ 137 | // case(#local_method_name(args)){ 138 | // // ... 139 | // }; 140 | // }; 141 | }; 142 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 143 | }; 144 | }; 145 | ``` 146 | 147 | - (Optional) Custom CallType.mo file. 148 | Modify the CallType.mo file according to your local tasks and the external tasks you need to call. 149 | 150 | - Creates a tpc object. 151 | ``` 152 | let tpc = TPCTM.TPCTM(Principal.fromActor(this), ?_local, ?_localAsync, ?_taskCallback, ?_orderCallback); 153 | ``` 154 | 155 | - Creates a transaction order. 156 | ``` 157 | let oid = tpc.create("TO_name", null, null); 158 | ``` 159 | 160 | - Pushs one or more transaction tasks to the specified transaction order. 161 | ``` 162 | let prepare: TPCTM.TaskRequest = { // for example 163 | callee = token_canister; 164 | callType = #DRC20(#lockTransferFrom(caller, to, value, 5*60, null, null, null, null)); // method to be called 165 | preTtid = []; // Pre-dependent tasks 166 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 167 | recallInterval = ?0; // nanoseconds 168 | cycles = 0; 169 | data = null; 170 | }; 171 | let commit: TPCTM.TaskRequest = { // for example 172 | callee = token_canister; 173 | callType = #DRC20(#executeTransfer(#AutoFill, #sendAll, null, null, null, null)); // method to be called 174 | preTtid = []; // Pre-dependent tasks 175 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 176 | recallInterval = ?0; // nanoseconds 177 | cycles = 0; 178 | data = null; 179 | }; 180 | let tid1 = tpc.push(oid, prepare, commit, null, null, null); 181 | ``` 182 | 183 | - Executes the transaction order. 184 | ``` 185 | tpc.close(oid); 186 | let res = await tpc.run(oid); 187 | ``` 188 | 189 | - Implements the upgrade function for TPCTM. 190 | ``` 191 | private stable var __tpcDataNew: ?TPCTM.Data = null; 192 | system func preupgrade() { 193 | let data = _getTPC().getData(); 194 | __tpcDataNew := ?data; 195 | // assert(List.size(data.actuator.tasks.0) == 0 and List.size(data.actuator.tasks.1) == 0); 196 | }; 197 | system func postupgrade() { 198 | switch(__tpcDataNew){ 199 | case(?(data)){ 200 | _getTPC().setData(data); 201 | __tpcDataNew := null; 202 | }; 203 | case(_){}; 204 | }; 205 | }; 206 | ``` 207 | 208 | ## Methods (API) 209 | 210 | ### ICTC(Saga) 211 | 212 | ``` 213 | public func create (_name: Text, _compStrategy: CompStrategy, _data: ?Blob, _callback: ?OrderCallback) : Toid 214 | 215 | public func push(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 216 | 217 | public func taskDone(_toid: Toid, _ttid: Ttid, _toCallback: Bool) : async ?Ttid 218 | 219 | public func redo(_toid: Toid, _ttid: Ttid) : ?Ttid 220 | 221 | public func open(_toid: Toid) : () 222 | 223 | public func close(_toid: Toid) : () 224 | 225 | public func run(_toid: Toid) : async ?OrderStatus 226 | 227 | public func asyncMessageSize() : Nat 228 | 229 | public func count() : Nat 230 | 231 | public func status(_toid: Toid) : ?OrderStatus 232 | 233 | public func isCompleted(_toid: Toid) : Bool 234 | 235 | public func isTaskCompleted(_ttid: Ttid) : Bool 236 | 237 | public func getOrder(_toid: Toid) : ?Order 238 | 239 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat} 240 | 241 | public func getAliveOrders() : [(Toid, ?Order)] 242 | 243 | public func getTaskEvents(_toid: Toid) : [TaskEvent] 244 | 245 | public func getActuator() : TA.TA 246 | 247 | public func setCacheExpiration(_expiration: Int) : () 248 | 249 | public func clear(_expiration: ?Int, _delForced: Bool) : () 250 | 251 | public func update(_toid: Toid, _ttid: Ttid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 252 | 253 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid 254 | 255 | public func append(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 256 | 257 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: PushCompRequest, _callback: ?Callback) : Tcid 258 | 259 | public func complete(_toid: Toid, _status: OrderStatus) : async Bool 260 | 261 | public func done(_toid: Toid, _status: OrderStatus, _toCallback: Bool) : async Bool 262 | 263 | public func block(_toid: Toid) : ?Toid 264 | 265 | public func getData() : Data 266 | 267 | public func setData(_data: Data) : () 268 | ``` 269 | 270 | ### ICTC(2PC) 271 | 272 | ``` 273 | public func create(_name: Text, _data: ?Blob, _callback: ?OrderCallback) : Toid 274 | 275 | public func push(_toid: Toid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 276 | 277 | public func taskDone(_toid: Toid, _ttid: Ttid, _toCallback: Bool) : async ?Ttid 278 | 279 | public func redo(_toid: Toid, _ttid: Ttid) : ?Ttid 280 | 281 | public func open(_toid: Toid) : () 282 | 283 | public func close(_toid: Toid) : () 284 | 285 | public func run(_toid: Toid) : async ?OrderStatus 286 | 287 | public func asyncMessageSize() : Nat 288 | 289 | public func count() : Nat 290 | 291 | public func status(_toid: Toid) : ?OrderStatus 292 | 293 | public func isCompleted(_toid: Toid) : Bool 294 | 295 | public func isTaskCompleted(_ttid: Ttid) : Bool 296 | 297 | public func getOrder(_toid: Toid) : ?Order 298 | 299 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat} 300 | 301 | public func getAliveOrders() : [(Toid, ?Order)] 302 | 303 | public func getTaskEvents(_toid: Toid) : [TaskEvent] 304 | 305 | public func getActuator() : TA.TA 306 | 307 | public func setCacheExpiration(_expiration: Int) : () 308 | 309 | public func clear(_expiration: ?Int, _delForced: Bool) : () 310 | 311 | public func update(_toid: Toid, _ttid: Ttid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 312 | 313 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid 314 | 315 | public func append(_toid: Toid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 316 | 317 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: TaskRequest, _callback: ?Callback) : Tcid 318 | 319 | public func complete(_toid: Toid, _status: OrderStatus) : async Bool 320 | 321 | public func done(_toid: Toid, _status: OrderStatus, _toCallback: Bool) : async Bool 322 | 323 | public func block(_toid: Toid) : ?Toid 324 | 325 | public func getData() : Data 326 | 327 | public func setData(_data: Data) : () 328 | ``` -------------------------------------------------------------------------------- /docs/ictc_reference-3.0.md: -------------------------------------------------------------------------------- 1 | # ICTC Reference 2 | 3 | ## Quickstart 4 | 5 | ### ICTC v3.0 (Saga) - Common implementations 6 | 7 | - Imports SagaTM Module. 8 | 9 | (use vessel package tool) 10 | ``` 11 | import SagaTM "mo:ictc/SagaTM"; 12 | ``` 13 | 14 | - Custom CallType. 15 | ``` 16 | public type CustomCallType = { 17 | // #This: { 18 | // #foo : (Nat); 19 | // }; 20 | }; 21 | ``` 22 | 23 | - (Optional) Implements callback functions for transaction orders and transaction tasks. 24 | ``` 25 | private func _taskCallback(_name: Text, _tid: SagaTM.Ttid, _task: SagaTM.Task, _result: SagaTM.TaskResult) : (){ 26 | // do something 27 | }; 28 | private func _orderCallback(_name: Text, _oid: SagaTM.Toid, _status: SagaTM.OrderStatus, _data: ?Blob) : (){ 29 | // do something 30 | }; 31 | ``` 32 | 33 | - Implements local tasks. (Each task needs to be internally atomic or able to maintain data consistency.) 34 | ``` 35 | private func _localCall(_callee: Principal, _cycles: Nat, _args: SagaTM.CallType, _receipt: ?SagaTM.Receipt) : async (TaskResult){ 36 | switch(_args){ 37 | case(#custom(#This(method))){ 38 | // switch(method){ 39 | // case(#foo(count)){ 40 | // var result = foo(count); // Receipt 41 | // return (#Done, ?#result(?(Binary.BigEndian.fromNat64(Nat64.fromNat(result)), debug_show(result))), null); 42 | // }; 43 | // }; 44 | }; 45 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 46 | }; 47 | }; 48 | ``` 49 | 50 | - Creates a saga object. 51 | ``` 52 | let saga = SagaTM.SagaTM(Principal.fromActor(this), ?_localCall, ?_taskCallback, ?_orderCallback); 53 | ``` 54 | 55 | - Creates a transaction order. (Supports `Forward` and `Backward` modes) 56 | ``` 57 | let oid = saga.create("TO_name", #Forward, null, null); 58 | ``` 59 | 60 | - Pushs one or more transaction tasks to the specified transaction order. 61 | ``` 62 | let task: SagaTM.PushTaskRequest = { // for example 63 | callee = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai"); 64 | callType = #ICRC1(#icrc1_transfer(_transferArgs_)); // method to be called 65 | preTtid = []; // Pre-dependent tasks 66 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 67 | recallInterval = ?0; // nanoseconds 68 | cycles = 0; 69 | data = null; 70 | }; 71 | let tid1 = saga.push(oid, task, null, null); 72 | ``` 73 | 74 | - Executes the transaction order. 75 | ``` 76 | saga.close(oid); 77 | let res = await saga.run(oid); 78 | ``` 79 | 80 | - Implements the upgrade function for SagaTM. 81 | ``` 82 | private stable var __sagaData: ?SagaTM.Data = null; 83 | system func preupgrade() { 84 | let data = _getSaga().getData(); 85 | __sagaData := ?data; 86 | // assert(List.size(data.actuator.tasks.0) == 0 and List.size(data.actuator.tasks.1) == 0); 87 | }; 88 | system func postupgrade() { 89 | switch(__sagaData){ 90 | case(?(data)){ 91 | _getSaga().setData(data); 92 | __sagaData := null; 93 | }; 94 | case(_){}; 95 | }; 96 | }; 97 | ``` 98 | 99 | ### ICTC v3.0 (2PC) 100 | 101 | - Imports SagaTM Module. 102 | 103 | (use vessel package tool) 104 | ``` 105 | import TPCTM "mo:ictc/TPCTM"; 106 | ``` 107 | 108 | - Custom CallType. 109 | ``` 110 | public type CustomCallType = { 111 | // #This: { 112 | // #foo : (Nat); 113 | // }; 114 | }; 115 | ``` 116 | 117 | - (Optional) Implements callback functions for transaction orders and transaction tasks. 118 | ``` 119 | private func _taskCallback(_name: Text, _tid: TPCTM.Ttid, _task: TPCTM.Task, _result: TPCTM.TaskResult) : (){ 120 | // do something 121 | }; 122 | private func _orderCallback(_name: Text, _oid: TPCTM.Toid, _status: TPCTM.OrderStatus, _data: ?Blob) : (){ 123 | // do something 124 | }; 125 | ``` 126 | 127 | - Implements local tasks. (Each task needs to be internally atomic or able to maintain data consistency.) 128 | ``` 129 | private func _localCall(_callee: Principal, _cycles: Nat, _args: TPCTM.CallType, _receipt: ?TPCTM.Receipt) : async (TPCTM.TaskResult){ 130 | switch(_args){ 131 | case(#custom(#This(method))){ 132 | // switch(method){ 133 | // case(#foo(count)){ 134 | // var result = foo(count); // Receipt 135 | // return (#Done, ?#result(?(Binary.BigEndian.fromNat64(Nat64.fromNat(result)), debug_show(result))), null); 136 | // }; 137 | // }; 138 | }; 139 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 140 | }; 141 | }; 142 | ``` 143 | 144 | - Creates a tpc object. 145 | ``` 146 | let tpc = TPCTM.TPCTM(Principal.fromActor(this), ?_localCall, ?_taskCallback, ?_orderCallback); 147 | ``` 148 | 149 | - Creates a transaction order. 150 | ``` 151 | let oid = tpc.create("TO_name", null, null); 152 | ``` 153 | 154 | - Pushs one or more transaction tasks to the specified transaction order. 155 | ``` 156 | let prepare: TPCTM.TaskRequest = { // for example 157 | callee = token_canister; 158 | callType = #DRC20(#drc20_lockTransferFrom(caller, to, value, 5*60, null, null, null, null)); // method to be called 159 | preTtid = []; // Pre-dependent tasks 160 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 161 | recallInterval = ?0; // nanoseconds 162 | cycles = 0; 163 | data = null; 164 | }; 165 | let commit: TPCTM.TaskRequest = { // for example 166 | callee = token_canister; 167 | callType = #DRC20(#drc20_executeTransfer(#AutoFill, #sendAll, null, null, null, null)); // method to be called 168 | preTtid = []; // Pre-dependent tasks 169 | attemptsMax = ?1; // Maximum number of repeat attempts in case of exception 170 | recallInterval = ?0; // nanoseconds 171 | cycles = 0; 172 | data = null; 173 | }; 174 | let tid1 = tpc.push(oid, prepare, commit, null, null, null); 175 | ``` 176 | 177 | - Executes the transaction order. 178 | ``` 179 | tpc.close(oid); 180 | let res = await tpc.run(oid); 181 | ``` 182 | 183 | - Implements the upgrade function for TPCTM. 184 | ``` 185 | private stable var __tpcData: ?TPCTM.Data = null; 186 | system func preupgrade() { 187 | let data = _getTPC().getData(); 188 | __tpcData := ?data; 189 | // assert(List.size(data.actuator.tasks.0) == 0 and List.size(data.actuator.tasks.1) == 0); 190 | }; 191 | system func postupgrade() { 192 | switch(__tpcData){ 193 | case(?(data)){ 194 | _getTPC().setData(data); 195 | __tpcData := null; 196 | }; 197 | case(_){}; 198 | }; 199 | }; 200 | ``` 201 | 202 | ## ICTC explorer 203 | 204 | https://cmqwp-uiaaa-aaaaj-aihzq-cai.raw.ic0.app/ 205 | 206 | ## Methods (API) 207 | 208 | ### ICTC(Saga) 209 | 210 | ``` 211 | public func create(_name: Text, _compStrategy: CompStrategy, _data: ?Blob, _callback: ?OrderCallback) : Toid 212 | 213 | public func push(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 214 | 215 | public func taskDone(_toid: Toid, _ttid: Ttid, _toCallback: Bool) : async* ?Ttid 216 | 217 | public func redo(_toid: Toid, _ttid: Ttid) : ?Ttid 218 | 219 | public func open(_toid: Toid) : () 220 | 221 | public func close(_toid: Toid) : () 222 | 223 | public func run(_toid: Toid) : async ?OrderStatus 224 | 225 | public func runSync(_toid: Toid) : async ?OrderStatus 226 | 227 | public func asyncMessageSize() : Nat 228 | 229 | public func count() : Nat 230 | 231 | public func status(_toid: Toid) : ?OrderStatus 232 | 233 | public func isCompleted(_toid: Toid) : Bool 234 | 235 | public func isTaskCompleted(_ttid: Ttid) : Bool 236 | 237 | public func getOrder(_toid: Toid) : ?Order 238 | 239 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat} 240 | 241 | public func getAliveOrders() : [(Toid, ?Order)] 242 | 243 | public func getBlockingOrders() : [(Toid, Order)] 244 | 245 | public func getTaskEvents(_toid: Toid) : [TaskEvent] 246 | 247 | public func getActuator() : TA.TA 248 | 249 | public func setCacheExpiration(_expiration: Int) : () 250 | 251 | public func clear(_expiration: ?Int, _delForced: Bool) : () 252 | 253 | public func update(_toid: Toid, _ttid: Ttid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 254 | 255 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid 256 | 257 | public func append(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?Callback) : Ttid 258 | 259 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: PushCompRequest, _callback: ?Callback) : Tcid 260 | 261 | public func complete(_toid: Toid, _status: OrderStatus) : async* Bool 262 | 263 | public func doneEmpty(_toid: Toid) : Bool 264 | 265 | public func done(_toid: Toid, _status: OrderStatus, _toCallback: Bool) : async* Bool 266 | 267 | public func block(_toid: Toid) : ?Toid 268 | 269 | public func getData() : Data 270 | 271 | public func setData(_data: Data) : () 272 | ``` 273 | 274 | ### ICTC(2PC) 275 | 276 | ``` 277 | public func create(_name: Text, _data: ?Blob, _callback: ?OrderCallback) : Toid 278 | 279 | public func push(_toid: Toid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 280 | 281 | public func taskDone(_toid: Toid, _ttid: Ttid, _toCallback: Bool) : async* ?Ttid 282 | 283 | public func redo(_toid: Toid, _ttid: Ttid) : ?Ttid 284 | 285 | public func open(_toid: Toid) : () 286 | 287 | public func close(_toid: Toid) : () 288 | 289 | public func run(_toid: Toid) : async ?OrderStatus 290 | 291 | public func runSync(_toid: Toid) : async ?OrderStatus 292 | 293 | public func asyncMessageSize() : Nat 294 | 295 | public func count() : Nat 296 | 297 | public func status(_toid: Toid) : ?OrderStatus 298 | 299 | public func isCompleted(_toid: Toid) : Bool 300 | 301 | public func isTaskCompleted(_ttid: Ttid) : Bool 302 | 303 | public func getOrder(_toid: Toid) : ?Order 304 | 305 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat} 306 | 307 | public func getAliveOrders() : [(Toid, ?Order)] 308 | 309 | public func getBlockingOrders() : [(Toid, Order)] 310 | 311 | public func getTaskEvents(_toid: Toid) : [TaskEvent] 312 | 313 | public func getActuator() : TA.TA 314 | 315 | public func setCacheExpiration(_expiration: Int) : () 316 | 317 | public func clear(_expiration: ?Int, _delForced: Bool) : () 318 | 319 | public func update(_toid: Toid, _ttid: Ttid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 320 | 321 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid 322 | 323 | public func append(_toid: Toid, _prepare: TaskRequest, _commit: TaskRequest, _comp: ?TaskRequest, _prepareCallback: ?Callback, _commitCallback: ?Callback) : Ttid 324 | 325 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: TaskRequest, _callback: ?Callback) : Tcid 326 | 327 | public func complete(_toid: Toid, _status: OrderStatus) : async* Bool 328 | 329 | public func doneEmpty(_toid: Toid) : Bool 330 | 331 | public func done(_toid: Toid, _status: OrderStatus, _toCallback: Bool) : async* Bool 332 | 333 | public func block(_toid: Toid) : ?Toid 334 | 335 | public func getData() : Data 336 | 337 | public func setData(_data: Data) : () 338 | ``` -------------------------------------------------------------------------------- /src/CallType.mo: -------------------------------------------------------------------------------- 1 | /** 2 | * Module : CallType.mo v3.0 3 | * Author : ICLighthouse Team 4 | * Stability : Experimental 5 | * Description: Wrapping the methods used by the transaction. 6 | * Refers : https://github.com/iclighthouse/ICTC 7 | */ 8 | 9 | import Blob "mo:base/Blob"; 10 | import Cycles "mo:base/ExperimentalCycles"; 11 | import DRC20 "mo:icl/DRC20"; 12 | import ICRC1 "mo:icl/ICRC1"; 13 | import ICRC2 "mo:icl/ICRC1"; 14 | import Error "mo:base/Error"; 15 | import Principal "mo:base/Principal"; 16 | 17 | module { 18 | public let Version: Nat = 10; 19 | public type Status = {#Todo; #Doing; #Done; #Error; #Unknown; }; 20 | public type Err = {code: Error.ErrorCode; message: Text; }; 21 | public type CallType = { 22 | #__skip; 23 | #__block; 24 | #custom: T; 25 | #ICRC1: { 26 | #icrc1_transfer : (ICRC1.TransferArgs); 27 | }; 28 | #ICRC2: { 29 | #icrc2_approve : (ICRC2.ApproveArgs); 30 | #icrc2_transfer_from : (ICRC2.TransferFromArgs); 31 | }; 32 | #DRC20: { 33 | #drc20_approve : (DRC20.Spender, DRC20.Amount, ?DRC20.Nonce, ?DRC20.Sa, ?DRC20.Data); 34 | #drc20_executeTransfer : (BlobFill, DRC20.ExecuteType, ?DRC20.To, ?DRC20.Nonce, ?DRC20.Sa, ?DRC20.Data); 35 | #drc20_lockTransfer : (DRC20.To, DRC20.Amount, DRC20.Timeout, ?DRC20.Decider, ?DRC20.Nonce, ?DRC20.Sa, ?DRC20.Data); 36 | #drc20_lockTransferFrom : (DRC20.From, DRC20.To, DRC20.Amount, DRC20.Timeout, ?DRC20.Decider, ?DRC20.Nonce, ?DRC20.Sa, ?DRC20.Data ); 37 | #drc20_transfer : (DRC20.To, DRC20.Amount, ?DRC20.Nonce, ?DRC20.Sa, ?DRC20.Data); 38 | #drc20_transferBatch : ([DRC20.To], [DRC20.Amount], ?DRC20.Nonce, ?DRC20.Sa, ?DRC20.Data); 39 | #drc20_transferFrom : (DRC20.From, DRC20.To, DRC20.Amount, ?DRC20.Nonce, ?DRC20.Sa, ?DRC20.Data); 40 | #drc20_dropAccount: ?DRC20.Sa; 41 | }; 42 | }; 43 | public type Return = ([Nat8], Text); 44 | public type Receipt = { 45 | #none; 46 | #result: ?Return; 47 | #ICRC1: { 48 | #icrc1_transfer : { #Ok: Nat; #Err: ICRC1.TransferError; }; 49 | }; 50 | #ICRC2: { 51 | #icrc2_approve : ({ #Ok : Nat; #Err : ICRC2.ApproveError }); 52 | #icrc2_transfer_from : ({ #Ok : Nat; #Err : ICRC2.TransferFromError }); 53 | }; 54 | #DRC20: { 55 | #drc20_approve : DRC20.TxnResult; 56 | #drc20_executeTransfer : DRC20.TxnResult; 57 | #drc20_lockTransfer : DRC20.TxnResult; 58 | #drc20_lockTransferFrom : DRC20.TxnResult; 59 | #drc20_transfer : DRC20.TxnResult; 60 | #drc20_transferBatch : [DRC20.TxnResult]; 61 | #drc20_transferFrom : DRC20.TxnResult; 62 | #drc20_dropAccount; 63 | }; 64 | }; 65 | public type TaskResult = (Status, receipt: ?Receipt, ?Err); 66 | public type CustomCall = (callee: Principal, cycles: Nat, CallType, ?Receipt) -> async (TaskResult); 67 | 68 | public type BlobFill = {#AutoFill; #ManualFill: Blob; }; 69 | public type NatFill = {#AutoFill; #ManualFill: Nat; }; 70 | 71 | // type ErrorCode = { 72 | // // Fatal error. 73 | // #system_fatal; 74 | // // Transient error. 75 | // #system_transient; 76 | // // Destination invalid. 77 | // #destination_invalid; 78 | // // Explicit reject by canister code. 79 | // #canister_reject; 80 | // // Canister trapped. 81 | // #canister_error; 82 | // // Future error code (with unrecognized numeric code) 83 | // #future : Nat32; 84 | // 9901 No such actor. 85 | // 9902 No such method. 86 | // 9903 Return #err by canister code. 87 | // 9904 Blocked by code. 88 | // }; 89 | 90 | /// Extracts the txid of DRC20 91 | public func DRC20Txid(txid: BlobFill, receipt: ?Receipt) : Blob{ 92 | var txid_ = Blob.fromArray([]); 93 | switch(txid){ 94 | case(#AutoFill){ 95 | switch(receipt){ 96 | case(?(#DRC20(#drc20_lockTransfer(#ok(v))))){ txid_ := v }; 97 | case(?(#DRC20(#drc20_lockTransferFrom(#ok(v))))){ txid_ := v }; 98 | case(?(#DRC20(#drc20_transfer(#ok(v))))){ txid_ := v }; 99 | case(?(#DRC20(#drc20_transferFrom(#ok(v))))){ txid_ := v }; 100 | case(?(#DRC20(#drc20_executeTransfer(#ok(v))))){ txid_ := v }; 101 | case(?(#DRC20(#drc20_approve(#ok(v))))){ txid_ := v }; 102 | case(_){}; 103 | }; 104 | }; 105 | case(#ManualFill(v)){ txid_ := v }; 106 | }; 107 | return txid_; 108 | }; 109 | 110 | /// Extracts the blockIndex of ICRC1/ICRC2 111 | public func ICRCBlockIndex(index: NatFill, receipt: ?Receipt) : Nat{ 112 | var index_ = 0; 113 | switch(index){ 114 | case(#AutoFill){ 115 | switch(receipt){ 116 | case(?(#ICRC1(#icrc1_transfer(#Ok(v))))){ index_ := v }; 117 | case(?(#ICRC2(#icrc2_approve(#Ok(v))))){ index_ := v }; 118 | case(?(#ICRC2(#icrc2_transfer_from(#Ok(v))))){ index_ := v }; 119 | case(_){}; 120 | }; 121 | }; 122 | case(#ManualFill(v)){ index_ := v }; 123 | }; 124 | return index_; 125 | }; 126 | 127 | /// Wrap the calling function 128 | public func call(_callee: Principal, _cycles: Nat, _call: CustomCall, _args: CallType, _receipt: ?Receipt) : async* TaskResult{ 129 | switch(_args){ 130 | case(#__skip){ return (#Done, ?#none, null); }; 131 | case(#__block){ return (#Error, ?#none, ?{code=#future(9904); message="Blocked by code."; }); }; 132 | case(#custom(T)){ 133 | try{ 134 | return await _call(_callee, _cycles, _args, _receipt); 135 | } catch (e){ 136 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 137 | }; 138 | }; 139 | case(#ICRC1(method)){ 140 | let token: ICRC1.Self = actor(Principal.toText(_callee)); 141 | if (_cycles > 0){ Cycles.add(_cycles); }; 142 | switch(method){ 143 | case(#icrc1_transfer(args)){ 144 | var result: { #Ok: Nat; #Err: ICRC1.TransferError; } = #Err(#TemporarilyUnavailable); // Receipt 145 | try{ 146 | // do 147 | result := await token.icrc1_transfer(args); 148 | // check & return 149 | switch(result){ 150 | case(#Ok(id)){ return (#Done, ?#ICRC1(#icrc1_transfer(result)), null); }; 151 | case(#Err(e)){ return (#Error, ?#ICRC1(#icrc1_transfer(result)), ?{code=#future(9903); message="ICRC1 token Err."; }); }; 152 | }; 153 | } catch (e){ 154 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 155 | }; 156 | }; 157 | //case(_){ return (#Error, null, ?{code=#future(9902); message="No such method."; });}; 158 | }; 159 | }; 160 | case(#ICRC2(method)){ 161 | let token: ICRC2.Self = actor(Principal.toText(_callee)); 162 | if (_cycles > 0){ Cycles.add(_cycles); }; 163 | switch(method){ 164 | case(#icrc2_approve(args)){ 165 | var result: { #Ok: Nat; #Err: ICRC2.ApproveError; } = #Err(#TemporarilyUnavailable); // Receipt 166 | try{ 167 | // do 168 | result := await token.icrc2_approve(args); 169 | // check & return 170 | switch(result){ 171 | case(#Ok(id)){ return (#Done, ?#ICRC2(#icrc2_approve(result)), null); }; 172 | case(#Err(e)){ return (#Error, ?#ICRC2(#icrc2_approve(result)), ?{code=#future(9903); message="ICRC2 token Err."; }); }; 173 | }; 174 | } catch (e){ 175 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 176 | }; 177 | }; 178 | case(#icrc2_transfer_from(args)){ 179 | var result: { #Ok: Nat; #Err: ICRC2.TransferFromError; } = #Err(#TemporarilyUnavailable); // Receipt 180 | try{ 181 | // do 182 | result := await token.icrc2_transfer_from(args); 183 | // check & return 184 | switch(result){ 185 | case(#Ok(id)){ return (#Done, ?#ICRC2(#icrc2_transfer_from(result)), null); }; 186 | case(#Err(e)){ return (#Error, ?#ICRC2(#icrc2_transfer_from(result)), ?{code=#future(9903); message="ICRC2 token Err."; }); }; 187 | }; 188 | } catch (e){ 189 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 190 | }; 191 | }; 192 | }; 193 | }; 194 | case(#DRC20(method)){ 195 | let token: DRC20.Self = actor(Principal.toText(_callee)); 196 | if (_cycles > 0){ Cycles.add(_cycles); }; 197 | switch(method){ 198 | case(#drc20_approve(spender, amount, nonce, sa, data)){ 199 | var result: DRC20.TxnResult = #err({code=#UndefinedError; message="No call."}); // Receipt 200 | try{ 201 | // do 202 | result := await token.drc20_approve(spender, amount, nonce, sa, data); 203 | // check & return 204 | switch(result){ 205 | case(#ok(txid)){ return (#Done, ?#DRC20(#drc20_approve(result)), null); }; 206 | case(#err(e)){ return (#Error, ?#DRC20(#drc20_approve(result)), ?{code=#future(9903); message=e.message; }); }; 207 | }; 208 | } catch (e){ 209 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 210 | }; 211 | }; 212 | case(#drc20_transfer(to, amount, nonce, sa, data)){ 213 | var result: DRC20.TxnResult = #err({code=#UndefinedError; message="No call."}); // Receipt 214 | try{ 215 | // do 216 | result := await token.drc20_transfer(to, amount, nonce, sa, data); 217 | // check & return 218 | switch(result){ 219 | case(#ok(txid)){ return (#Done, ?#DRC20(#drc20_transfer(result)), null); }; 220 | case(#err(e)){ return (#Error, ?#DRC20(#drc20_transfer(result)), ?{code=#future(9903); message=e.message; }); }; 221 | }; 222 | } catch (e){ 223 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 224 | }; 225 | }; 226 | case(#drc20_transferBatch(to, amount, nonce, sa, data)){ 227 | var results: [DRC20.TxnResult] = []; // Receipt 228 | try{ 229 | // do 230 | results := await token.drc20_transferBatch(to, amount, nonce, sa, data); 231 | // check & return 232 | var isSuccess : Bool = true; 233 | for (result in results.vals()){ 234 | switch(result){ 235 | case(#ok(txid)){}; 236 | case(#err(e)){ isSuccess := false; }; 237 | }; 238 | }; 239 | if (isSuccess){ 240 | return (#Done, ?#DRC20(#drc20_transferBatch(results)), null); 241 | }else{ 242 | return (#Error, ?#DRC20(#drc20_transferBatch(results)), ?{code=#future(9903); message="Batch transaction error."; }); 243 | }; 244 | } catch (e){ 245 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 246 | }; 247 | }; 248 | case(#drc20_transferFrom(from, to, amount, nonce, sa, data)){ 249 | var result: DRC20.TxnResult = #err({code=#UndefinedError; message="No call."}); // Receipt 250 | try{ 251 | // do 252 | result := await token.drc20_transferFrom(from, to, amount, nonce, sa, data); 253 | // check & return 254 | switch(result){ 255 | case(#ok(txid)){ return (#Done, ?#DRC20(#drc20_transferFrom(result)), null); }; 256 | case(#err(e)){ return (#Error, ?#DRC20(#drc20_transferFrom(result)), ?{code=#future(9903); message=e.message; }); }; 257 | }; 258 | } catch (e){ 259 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 260 | }; 261 | }; 262 | case(#drc20_lockTransfer(to, amount, timeout, decider, nonce, sa, data)){ 263 | var result: DRC20.TxnResult = #err({code=#UndefinedError; message="No call."}); // Receipt 264 | try{ 265 | // do 266 | result := await token.drc20_lockTransfer(to, amount, timeout, decider, nonce, sa, data); 267 | // check & return 268 | switch(result){ 269 | case(#ok(txid)){ return (#Done, ?#DRC20(#drc20_lockTransfer(result)), null); }; 270 | case(#err(e)){ return (#Error, ?#DRC20(#drc20_lockTransfer(result)), ?{code=#future(9903); message=e.message; }); }; 271 | }; 272 | } catch (e){ 273 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 274 | }; 275 | }; 276 | case(#drc20_lockTransferFrom(from, to, amount, timeout, decider, nonce, sa, data)){ 277 | var result: DRC20.TxnResult = #err({code=#UndefinedError; message="No call."}); // Receipt 278 | try{ 279 | // do 280 | result := await token.drc20_lockTransferFrom(from, to, amount, timeout, decider, nonce, sa, data); 281 | // check & return 282 | switch(result){ 283 | case(#ok(txid)){ return (#Done, ?#DRC20(#drc20_lockTransferFrom(result)), null); }; 284 | case(#err(e)){ return (#Error, ?#DRC20(#drc20_lockTransferFrom(result)), ?{code=#future(9903); message=e.message; }); }; 285 | }; 286 | } catch (e){ 287 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 288 | }; 289 | }; 290 | case(#drc20_executeTransfer(txid, executeType, to, nonce, sa, data)){ 291 | let txid_ = DRC20Txid(txid, _receipt); 292 | var result: DRC20.TxnResult = #err({code=#UndefinedError; message="No call."}); // Receipt 293 | try{ 294 | // do 295 | result := await token.drc20_executeTransfer(txid_, executeType, to, nonce, sa, data); 296 | // check & return 297 | switch(result){ 298 | case(#ok(txid)){ return (#Done, ?#DRC20(#drc20_executeTransfer(result)), null); }; 299 | case(#err(e)){ return (#Error, ?#DRC20(#drc20_executeTransfer(result)), ?{code=#future(9903); message=e.message; }); }; 300 | }; 301 | } catch (e){ 302 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 303 | }; 304 | }; 305 | case(#drc20_dropAccount(_sa)){ 306 | try{ 307 | // do 308 | let _f = token.drc20_dropAccount(_sa); 309 | // check & return 310 | return (#Done, ?#DRC20(#drc20_dropAccount), null); 311 | } catch (e){ 312 | return (#Error, null, ?{code=Error.code(e); message=Error.message(e); }); 313 | }; 314 | }; 315 | //case(_){ return (#Error, null, ?{code=#future(9902); message="No such method."; });}; 316 | }; 317 | }; 318 | }; 319 | 320 | }; 321 | 322 | }; -------------------------------------------------------------------------------- /examples/Example2PC.mo: -------------------------------------------------------------------------------- 1 | /** 2 | * Module : ICTC 2PC Test 3 | * Author : ICLighthouse Team 4 | * Stability : Experimental 5 | * Github : https://github.com/iclighthouse/ICTC/ 6 | */ 7 | 8 | import Array "mo:base/Array"; 9 | import Blob "mo:base/Blob"; 10 | import Nat "mo:base/Nat"; 11 | import Nat64 "mo:base/Nat64"; 12 | import Time "mo:base/Time"; 13 | import Option "mo:base/Option"; 14 | import Error "mo:base/Error"; 15 | import Principal "mo:base/Principal"; 16 | import Binary "mo:icl/Binary"; 17 | import CallType "../src/CallType"; 18 | import TA "../src/TA"; 19 | import TPCTM "../src/TPCTM"; 20 | 21 | shared(installMsg) actor class Example() = this { 22 | public type CustomCallType = { 23 | #This: { 24 | #foo : (Nat); 25 | }; 26 | }; 27 | type CallType = CallType.CallType; 28 | type Task = TPCTM.Task; 29 | type TaskResult = CallType.TaskResult; 30 | private var tokenA_canister = Principal.fromText("ueghb-uqaaa-aaaak-aaioa-cai"); 31 | private var tokenB_canister = Principal.fromText("udhbv-ziaaa-aaaak-aaioq-cai"); 32 | private var x : Nat = 0; 33 | private func foo(_count: Nat) : Nat{ 34 | x += _count; 35 | return x; 36 | }; 37 | private func _localCall(_callee: Principal, _cycles: Nat, _args: CallType, _receipt: ?TPCTM.Receipt) : async (TaskResult){ 38 | switch(_args){ 39 | case(#custom(#This(method))){ 40 | switch(method){ 41 | case(#foo(count)){ 42 | var result = foo(count); // Receipt 43 | return (#Done, ?#result(?(Binary.BigEndian.fromNat64(Nat64.fromNat(result)), debug_show(result))), null); 44 | }; 45 | }; 46 | }; 47 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 48 | }; 49 | }; 50 | private func _taskCallback(_name: Text, _tid: TPCTM.Ttid, _task: Task, _result: TaskResult) : async (){ 51 | taskLogs := TA.arrayAppend(taskLogs, [(_tid, _task, _result)]); 52 | }; 53 | private func _orderCallback(_name: Text, _oid: TPCTM.Toid, _status: TPCTM.OrderStatus, _data: ?Blob) : async (){ 54 | orderLogs := TA.arrayAppend(orderLogs, [(_oid, _status)]); 55 | }; 56 | 57 | private stable var taskLogs: [(TPCTM.Ttid, Task, TaskResult)] = []; 58 | private stable var orderLogs: [(TPCTM.Toid, TPCTM.OrderStatus)] = []; 59 | private var tpc: ?TPCTM.TPCTM = null; 60 | private func _getTPC() : TPCTM.TPCTM { 61 | switch(tpc){ 62 | case(?(_tpc)){ return _tpc }; 63 | case(_){ 64 | let _tpc = TPCTM.TPCTM(Principal.fromActor(this), ?_localCall, ?_taskCallback, ?_orderCallback); //?_taskCallback, ?_orderCallback 65 | tpc := ?_tpc; 66 | return _tpc; 67 | }; 68 | }; 69 | }; 70 | 71 | public query func getX() : async Nat{ 72 | x; 73 | }; 74 | public query func getTaskLogs() : async [(TPCTM.Ttid, Task, TaskResult)]{ 75 | return taskLogs; 76 | }; 77 | public query func getOrderLogs() : async [(TPCTM.Toid, TPCTM.OrderStatus)]{ 78 | return orderLogs; 79 | }; 80 | public shared func clearLogs() : async (){ 81 | taskLogs := []; 82 | orderLogs := []; 83 | }; 84 | public shared func claimTestTokens(_account: Text) : async (){ 85 | let act = TA.TA(50, 24*3600*1000000000, _localCall, null, ?_taskCallback); 86 | var task = _task(tokenA_canister, #DRC20(#drc20_transfer(_account, 5000000000, null, null, null))); 87 | let _tid1 = act.push(task); 88 | task := _task(tokenB_canister, #DRC20(#drc20_transfer(_account, 5000000000, null, null, null))); 89 | let _tid2 = act.push(task); 90 | let _f = act.run(); 91 | }; 92 | 93 | 94 | private func _buildTask(_businessId: ?Blob, _callee: Principal, _callType: CallType, _preTtids: [TPCTM.Ttid]) : TPCTM.TaskRequest{ 95 | return { 96 | callee = _callee; 97 | callType = _callType; 98 | preTtid = _preTtids; 99 | attemptsMax = ?1; 100 | recallInterval = ?0; // nanoseconds 101 | cycles = 0; 102 | data = _businessId; 103 | }; 104 | }; 105 | private func _task(_callee: Principal, _callType: CallType) : Task{ 106 | return { 107 | callee = _callee; 108 | callType = _callType; 109 | preTtid = []; 110 | toid = null; 111 | forTtid = null; 112 | attemptsMax = 3; 113 | recallInterval = (5*1000000000); // nanoseconds 114 | cycles = 0; 115 | data = null; 116 | time = Time.now(); 117 | }; 118 | }; 119 | 120 | public shared(msg) func swap1(_to: Text): async (TPCTM.Toid, ?TPCTM.OrderStatus){ 121 | let valueA: Nat = 100000000; 122 | let valueB: Nat = 200000000; 123 | let caller: Text = Principal.toText(msg.caller); 124 | let to: Text = _to; 125 | 126 | // Transaction: 127 | /// prepare 128 | // tokenA: lockTransferFrom caller -> to 1.00000010 129 | // tokenB: lockTransferFrom to -> caller 2.00000010 130 | /// commit 131 | // tokenA: executeTransfer $tx1 #sendAll 132 | // tokenB: executeTransfer $tx2 #sendAll 133 | /// compensate 134 | // tokenA: executeTransfer $tx1 #fallback 135 | // tokenB: executeTransfer $tx2 #fallback 136 | 137 | let oid = _getTPC().create("swap1", null, null); 138 | var prepare = _buildTask(null, tokenA_canister, #DRC20(#drc20_lockTransferFrom(caller, to, valueA, 5*60, null, null, null, null)), []); 139 | var commit = _buildTask(null, tokenA_canister, #DRC20(#drc20_executeTransfer(#AutoFill, #sendAll, null, null, null, null)), []); 140 | var comp = _buildTask(null, tokenA_canister, #DRC20(#drc20_executeTransfer(#AutoFill, #fallback, null, null, null, null)), []); 141 | let _tid1 = _getTPC().push(oid, prepare, commit, ?comp, null, null); 142 | prepare := _buildTask(null, tokenB_canister, #DRC20(#drc20_lockTransferFrom(to, caller, valueB, 5*60, null, null, null, null)), []); 143 | commit := _buildTask(null, tokenB_canister, #DRC20(#drc20_executeTransfer(#AutoFill, #sendAll, null, null, null, null)), []); 144 | comp := _buildTask(null, tokenB_canister, #DRC20(#drc20_executeTransfer(#AutoFill, #fallback, null, null, null, null)), []); 145 | let _tid2 = _getTPC().push(oid, prepare, commit, ?comp, null, null); 146 | _getTPC().close(oid); 147 | let res = await _getTPC().run(oid); 148 | return (oid, res); 149 | }; 150 | 151 | public shared(msg) func swap2(_to: Text): async (TPCTM.Toid, ?TPCTM.OrderStatus){ // Blocking 152 | let valueA: Nat = 100000000; 153 | let valueB: Nat = 200000000; 154 | let caller: Text = Principal.toText(msg.caller); 155 | let to: Text = _to; 156 | 157 | // Transaction: 158 | /// prepare 159 | // tokenA: lockTransferFrom caller -> to 1.00000010 160 | // tokenB: lockTransferFrom to -> caller 2.00000010 161 | /// commit 162 | // tokenA: executeTransfer $tx1 #sendAll 163 | // tokenB: executeTransfer $tx2 #sendAll 164 | /// compensate 165 | // tokenA: executeTransfer $tx1 #fallback 166 | // tokenB: executeTransfer $tx2 #fallback 167 | 168 | let oid = _getTPC().create("swap2", null, null); 169 | var prepare = _buildTask(null, tokenA_canister, #DRC20(#drc20_lockTransferFrom(caller, to, valueA, 5*60, null, null, null, null)), []); 170 | var commit = _buildTask(null, tokenA_canister, #DRC20(#drc20_executeTransfer(#AutoFill, #sendAll, null, null, null, null)), []); 171 | var comp = _buildTask(null, tokenA_canister, #DRC20(#drc20_executeTransfer(#AutoFill, #fallback, null, null, null, null)), []); 172 | let _tid1 = _getTPC().push(oid, prepare, commit, ?comp, null, null); 173 | prepare := _buildTask(null, tokenB_canister, #DRC20(#drc20_lockTransferFrom(to, caller, valueB, 5*60, null, null, null, null)), []); 174 | commit := _buildTask(null, tokenB_canister, #DRC20(#drc20_executeTransfer(#ManualFill(Blob.fromArray([])), #sendAll, null, null, null, null)), []); 175 | comp := _buildTask(null, tokenB_canister, #DRC20(#drc20_executeTransfer(#ManualFill(Blob.fromArray([])), #fallback, null, null, null, null)), []); 176 | let _tid2 = _getTPC().push(oid, prepare, commit, ?comp, null, null); 177 | _getTPC().close(oid); 178 | let res = await _getTPC().run(oid); 179 | return (oid, res); 180 | }; 181 | 182 | 183 | /** 184 | * ICTC Transaction Explorer Interface 185 | * (Optional) Implement the following interface, which allows you to browse transaction records and execute compensation transactions through a UI interface. 186 | * https://cmqwp-uiaaa-aaaaj-aihzq-cai.raw.ic0.app/ 187 | */ 188 | // ICTC: management functions 189 | private stable var ictc_admins: [Principal] = [installMsg.caller]; 190 | private func _onlyIctcAdmin(_caller: Principal) : Bool { 191 | return Option.isSome(Array.find(ictc_admins, func (t: Principal): Bool{ t == _caller })); 192 | }; 193 | private func _onlyBlocking(_toid: Nat) : Bool{ 194 | /// Saga 195 | // switch(_getTPC().status(_toid)){ 196 | // case(?(status)){ return status == #Blocking }; 197 | // case(_){ return false; }; 198 | // }; 199 | /// 2PC 200 | switch(_getTPC().status(_toid)){ 201 | case(?(status)){ return status == #Blocking }; 202 | case(_){ return false; }; 203 | }; 204 | }; 205 | public query func ictc_getAdmins() : async [Principal]{ 206 | return ictc_admins; 207 | }; 208 | public shared(msg) func ictc_addAdmin(_admin: Principal) : async (){ 209 | assert(_onlyIctcAdmin(msg.caller)); 210 | if (Option.isNull(Array.find(ictc_admins, func (t: Principal): Bool{ t == _admin }))){ 211 | ictc_admins := TA.arrayAppend(ictc_admins, [_admin]); 212 | }; 213 | }; 214 | public shared(msg) func ictc_removeAdmin(_admin: Principal) : async (){ 215 | assert(_onlyIctcAdmin(msg.caller)); 216 | ictc_admins := Array.filter(ictc_admins, func (t: Principal): Bool{ t != _admin }); 217 | }; 218 | 219 | // TPCTM Scan 220 | public query func ictc_TM() : async Text{ 221 | return "2PC"; 222 | }; 223 | /// 2PC 224 | public query func ictc_2PC_getTOCount() : async Nat{ 225 | return _getTPC().count(); 226 | }; 227 | public query func ictc_2PC_getTO(_toid: TPCTM.Toid) : async ?TPCTM.Order{ 228 | return _getTPC().getOrder(_toid); 229 | }; 230 | public query func ictc_2PC_getTOs(_page: Nat, _size: Nat) : async {data: [(TPCTM.Toid, TPCTM.Order)]; totalPage: Nat; total: Nat}{ 231 | return _getTPC().getOrders(_page, _size); 232 | }; 233 | public query func ictc_2PC_getTOPool() : async [(TPCTM.Toid, ?TPCTM.Order)]{ 234 | return _getTPC().getAliveOrders(); 235 | }; 236 | public query func ictc_2PC_getTT(_ttid: TPCTM.Ttid) : async ?TPCTM.TaskEvent{ 237 | return _getTPC().getActuator().getTaskEvent(_ttid); 238 | }; 239 | public query func ictc_2PC_getTTByTO(_toid: TPCTM.Toid) : async [TPCTM.TaskEvent]{ 240 | return _getTPC().getTaskEvents(_toid); 241 | }; 242 | public query func ictc_2PC_getTTs(_page: Nat, _size: Nat) : async {data: [(TPCTM.Ttid, TPCTM.TaskEvent)]; totalPage: Nat; total: Nat}{ 243 | return _getTPC().getActuator().getTaskEvents(_page, _size); 244 | }; 245 | public query func ictc_2PC_getTTPool() : async [(TPCTM.Ttid, Task)]{ 246 | let pool = _getTPC().getActuator().getTaskPool(); 247 | let arr = Array.map<(TPCTM.Ttid, Task), (TPCTM.Ttid, Task)>(pool, 248 | func (item:(TPCTM.Ttid, Task)): (TPCTM.Ttid, Task){ 249 | (item.0, item.1); 250 | }); 251 | return arr; 252 | }; 253 | public query func ictc_2PC_getTTErrors(_page: Nat, _size: Nat) : async {data: [(Nat, TPCTM.ErrorLog)]; totalPage: Nat; total: Nat}{ 254 | return _getTPC().getActuator().getErrorLogs(_page, _size); 255 | }; 256 | public query func ictc_2PC_getCalleeStatus(_callee: Principal) : async ?TPCTM.CalleeStatus{ 257 | return _getTPC().getActuator().calleeStatus(_callee); 258 | }; 259 | 260 | // Transaction Governance 261 | public shared(msg) func ictc_clearLog(_expiration: ?Int, _delForced: Bool) : async (){ // Warning: Execute this method with caution 262 | assert(_onlyIctcAdmin(msg.caller)); 263 | _getTPC().clear(_expiration, _delForced); 264 | }; 265 | public shared(msg) func ictc_clearTTPool() : async (){ // Warning: Execute this method with caution 266 | assert(_onlyIctcAdmin(msg.caller)); 267 | _getTPC().getActuator().clearTasks(); 268 | }; 269 | public shared(msg) func ictc_blockTO(_toid: TPCTM.Toid) : async ?TPCTM.Toid{ 270 | assert(_onlyIctcAdmin(msg.caller)); 271 | assert(not(_onlyBlocking(_toid))); 272 | let saga = _getTPC(); 273 | return saga.block(_toid); 274 | }; 275 | public shared(msg) func ictc_2PC_blockTO(_toid: TPCTM.Toid) : async ?TPCTM.Toid{ 276 | assert(_onlyIctcAdmin(msg.caller)); 277 | assert(not(_onlyBlocking(_toid))); 278 | let tpc = _getTPC(); 279 | return tpc.block(_toid); 280 | }; 281 | // public shared(msg) func ictc_2PC_removeTT(_toid: TPCTM.Toid, _ttid: TPCTM.Ttid) : async ?TPCTM.Ttid{ // Warning: Execute this method with caution 282 | // assert(_onlyBlocking(_toid)); 283 | // let tpc = _getTPC(); 284 | // tpc.open(_toid); 285 | // let ttid = tpc.remove(_toid, _ttid); 286 | // tpc.close(_toid); 287 | // return ttid; 288 | // }; 289 | public shared(msg) func ictc_2PC_appendTT(_businessId: ?Blob, _toid: TPCTM.Toid, _forTtid: ?TPCTM.Ttid, _callee: Principal, _callType: CallType, _preTtids: [TPCTM.Ttid]) : async TPCTM.Ttid{ 290 | // Governance or manual compensation (operation allowed only when a transaction order is in blocking status). 291 | assert(_onlyIctcAdmin(msg.caller)); 292 | assert(_onlyBlocking(_toid)); 293 | let tpc = _getTPC(); 294 | tpc.open(_toid); 295 | let taskRequest = _buildTask(_businessId, _callee, _callType, _preTtids); 296 | //let ttid = tpc.append(_toid, taskRequest, null, null); 297 | let ttid = tpc.appendComp(_toid, Option.get(_forTtid, 0), taskRequest, null); 298 | return ttid; 299 | }; 300 | /// Try the task again 301 | public shared(msg) func ictc_2PC_redoTT(_toid: TPCTM.Toid, _ttid: TPCTM.Ttid) : async ?TPCTM.Ttid{ 302 | // Warning: proceed with caution! 303 | assert(_onlyIctcAdmin(msg.caller)); 304 | let tpc = _getTPC(); 305 | let ttid = tpc.redo(_toid, _ttid); 306 | try{ 307 | let _r = await tpc.run(_toid); 308 | }catch(e){ 309 | throw Error.reject("430: ICTC error: "# Error.message(e)); 310 | }; 311 | return ttid; 312 | }; 313 | /// set status of pending task 314 | public shared(msg) func ictc_2PC_doneTT(_toid: TPCTM.Toid, _ttid: TPCTM.Ttid, _toCallback: Bool) : async ?TPCTM.Ttid{ 315 | // Warning: proceed with caution! 316 | assert(_onlyIctcAdmin(msg.caller)); 317 | let tpc = _getTPC(); 318 | try{ 319 | let ttid = await* tpc.taskDone(_toid, _ttid, _toCallback); 320 | return ttid; 321 | }catch(e){ 322 | throw Error.reject("420: internal call error: "# Error.message(e)); 323 | }; 324 | }; 325 | /// set status of pending order 326 | public shared(msg) func ictc_2PC_doneTO(_toid: TPCTM.Toid, _status: TPCTM.OrderStatus, _toCallback: Bool) : async Bool{ 327 | // Warning: proceed with caution! 328 | assert(_onlyIctcAdmin(msg.caller)); 329 | let tpc = _getTPC(); 330 | tpc.close(_toid); 331 | try{ 332 | let res = await* tpc.done(_toid, _status, _toCallback); 333 | return res; 334 | }catch(e){ 335 | throw Error.reject("420: internal call error: "# Error.message(e)); 336 | }; 337 | }; 338 | public shared(msg) func ictc_2PC_completeTO(_toid: TPCTM.Toid, _status: TPCTM.OrderStatus) : async Bool{ 339 | // After governance or manual compensations, this method needs to be called to complete the transaction order. 340 | assert(_onlyIctcAdmin(msg.caller)); 341 | assert(_onlyBlocking(_toid)); 342 | let tpc = _getTPC(); 343 | tpc.close(_toid); 344 | let _r = await tpc.run(_toid); 345 | return await* _getTPC().complete(_toid, _status); 346 | }; 347 | public shared(msg) func ictc_2PC_runTO(_toid: TPCTM.Toid) : async ?TPCTM.OrderStatus{ 348 | assert(_onlyIctcAdmin(msg.caller)); 349 | let tpc = _getTPC(); 350 | tpc.close(_toid); 351 | try{ 352 | let r = await tpc.run(_toid); 353 | return r; 354 | }catch(e){ 355 | throw Error.reject("430: ICTC error: "# Error.message(e)); 356 | }; 357 | }; 358 | public shared(msg) func ictc_2PC_runTT() : async Bool{ 359 | // There is no need to call it normally, but can be called if you want to execute tasks in time when a TO is in the Doing state. 360 | assert(_onlyIctcAdmin(msg.caller)); 361 | let tpc = _getTPC(); 362 | try{ 363 | let _r = await tpc.run(0); 364 | }catch(e){ 365 | throw Error.reject("430: ICTC error: "# Error.message(e)); 366 | }; 367 | return true; 368 | }; 369 | /** 370 | * End: ICTC Transaction Explorer Interface 371 | */ 372 | 373 | 374 | 375 | // upgrade 376 | private stable var __tpcDataNew: ?TPCTM.Data = null; 377 | system func preupgrade() { 378 | let data = _getTPC().getData(); 379 | __tpcDataNew := ?data; 380 | // assert(List.size(data.actuator.tasks.0) == 0 and List.size(data.actuator.tasks.1) == 0); 381 | }; 382 | system func postupgrade() { 383 | switch(__tpcDataNew){ 384 | case(?(data)){ 385 | _getTPC().setData(data); 386 | __tpcDataNew := null; 387 | }; 388 | case(_){}; 389 | }; 390 | }; 391 | 392 | 393 | }; -------------------------------------------------------------------------------- /examples/Example.mo: -------------------------------------------------------------------------------- 1 | /** 2 | * Module : ICTC Saga Test 3 | * Author : ICLighthouse Team 4 | * Stability : Experimental 5 | * Github : https://github.com/iclighthouse/ICTC/ 6 | */ 7 | 8 | import Array "mo:base/Array"; 9 | import Blob "mo:base/Blob"; 10 | import Nat "mo:base/Nat"; 11 | import Nat64 "mo:base/Nat64"; 12 | // import Iter "mo:base/Iter"; 13 | import Time "mo:base/Time"; 14 | import Option "mo:base/Option"; 15 | import Error "mo:base/Error"; 16 | import Principal "mo:base/Principal"; 17 | import Binary "mo:icl/Binary"; 18 | import CallType "../src/CallType"; 19 | import TA "../src/TA"; 20 | import SagaTM "../src/SagaTM"; 21 | 22 | shared(installMsg) actor class Example() = this { 23 | public type CustomCallType = { 24 | #This: { 25 | #foo : (Nat); 26 | }; 27 | }; 28 | type CallType = CallType.CallType; 29 | type Task = SagaTM.Task; 30 | type TaskResult = CallType.TaskResult; 31 | private var tokenA_canister = Principal.fromText("ueghb-uqaaa-aaaak-aaioa-cai"); 32 | private var tokenB_canister = Principal.fromText("udhbv-ziaaa-aaaak-aaioq-cai"); 33 | private var x : Nat = 0; 34 | private func slice(a: [T], from: Nat, to: ?Nat): [T]{ 35 | let len = a.size(); 36 | if (len == 0) { return []; }; 37 | var to_: Nat = Option.get(to, Nat.sub(len, 1)); 38 | if (len <= to_){ to_ := len - 1; }; 39 | var na: [T] = []; 40 | var i: Nat = from; 41 | while ( i <= to_ ){ 42 | na := TA.arrayAppend(na, Array.make(a[i])); 43 | i += 1; 44 | }; 45 | return na; 46 | }; 47 | private func foo(_count: Nat) : Nat{ 48 | x += _count; 49 | return x; 50 | }; 51 | private func _localCall(_callee: Principal, _cycles: Nat, _args: CallType, _receipt: ?SagaTM.Receipt) : async (TaskResult){ 52 | switch(_args){ 53 | case(#custom(#This(method))){ 54 | switch(method){ 55 | case(#foo(count)){ 56 | var result = foo(count); // Receipt 57 | return (#Done, ?#result(?(Binary.BigEndian.fromNat64(Nat64.fromNat(result)), debug_show(result))), null); 58 | }; 59 | }; 60 | }; 61 | case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 62 | }; 63 | }; 64 | // private func _localAsync(_args: SagaTM.CallType, _receipt: ?SagaTM.Receipt) : async (TaskResult){ 65 | // switch(_args){ 66 | // case(#This(method)){ 67 | // switch(method){ 68 | // case(#foo(count)){ 69 | // var result = foo(count); // Receipt 70 | // return (#Done, ?#This(#foo(result)), null); 71 | // }; 72 | // }; 73 | // }; 74 | // case(_){ return (#Error, null, ?{code=#future(9901); message="Non-local function."; });}; 75 | // }; 76 | // }; 77 | private func _taskCallback(_name: Text, _tid: SagaTM.Ttid, _task: Task, _result: TaskResult) : async (){ 78 | taskLogs := TA.arrayAppend(taskLogs, [(_tid, _task, _result)]); 79 | }; 80 | private func _orderCallback(_name: Text, _oid: SagaTM.Toid, _status: SagaTM.OrderStatus, _data: ?Blob) : async (){ 81 | orderLogs := TA.arrayAppend(orderLogs, [(_oid, _status)]); 82 | }; 83 | 84 | private stable var taskLogs: [(SagaTM.Ttid, Task, TaskResult)] = []; 85 | private stable var orderLogs: [(SagaTM.Toid, SagaTM.OrderStatus)] = []; 86 | private var saga: ?SagaTM.SagaTM = null; 87 | private func _getSaga() : SagaTM.SagaTM { 88 | switch(saga){ 89 | case(?(_saga)){ return _saga }; 90 | case(_){ 91 | let _saga = SagaTM.SagaTM(Principal.fromActor(this), ?_localCall, ?_taskCallback, ?_orderCallback); //?_taskCallback, ?_orderCallback 92 | saga := ?_saga; 93 | return _saga; 94 | }; 95 | }; 96 | }; 97 | 98 | public query func getX() : async Nat{ 99 | x; 100 | }; 101 | public query func getTaskLogs() : async [(SagaTM.Ttid, Task, TaskResult)]{ 102 | return taskLogs; 103 | }; 104 | public query func getOrderLogs() : async [(SagaTM.Toid, SagaTM.OrderStatus)]{ 105 | return orderLogs; 106 | }; 107 | public shared func clearLogs() : async (){ 108 | taskLogs := []; 109 | orderLogs := []; 110 | }; 111 | public shared func claimTestTokens(_account: Text) : async (){ 112 | let act = TA.TA(50, 24*3600*1000000000, _localCall, null, ?_taskCallback); 113 | var task = _task(tokenA_canister, #DRC20(#drc20_transfer(_account, 5000000000, null, null, null)), []); 114 | let _tid1 = act.push(task); 115 | task := _task(tokenB_canister, #DRC20(#drc20_transfer(_account, 5000000000, null, null, null)), []); 116 | let _tid2 = act.push(task); 117 | let _f = act.run(); 118 | }; 119 | 120 | 121 | private func _buildTask(_businessId: ?Blob, _callee: Principal, _callType: CallType, _preTtid: [SagaTM.Ttid]) : SagaTM.PushTaskRequest{ 122 | return { 123 | callee = _callee; 124 | callType = _callType; 125 | preTtid = _preTtid; 126 | attemptsMax = ?1; 127 | recallInterval = ?0; // nanoseconds 128 | cycles = 0; 129 | data = _businessId; 130 | }; 131 | }; 132 | private func _task(_callee: Principal, _callType: CallType, _preTtid: [SagaTM.Ttid]) : Task{ 133 | return { 134 | callee = _callee; 135 | callType = _callType; 136 | preTtid = _preTtid; 137 | toid = null; 138 | forTtid = null; 139 | attemptsMax = 3; 140 | recallInterval = (5*1000000000); // nanoseconds 141 | cycles = 0; 142 | data = null; 143 | time = Time.now(); 144 | }; 145 | }; 146 | 147 | public shared(msg) func swap1(_to: Text): async (SagaTM.Toid, ?SagaTM.OrderStatus){ 148 | let valueA: Nat = 100000000; 149 | let valueB: Nat = 200000000; 150 | let tokenFee: Nat = 100000; 151 | let caller: Text = Principal.toText(msg.caller); 152 | let to: Text = _to; 153 | let contract: Text = Principal.toText(Principal.fromActor(this)); 154 | 155 | // Transaction: 156 | // tokenA: transferFrom caller -> contract 1.00000010 157 | // tokenB: transferFrom to -> contract 2.00000010 158 | // local: _foo x+100 159 | // tokenA: transfer contract -> to 1.00000000 160 | // tokenB: transfer contract -> caller 2.00000000 161 | 162 | let oid = _getSaga().create("swap1", #Forward, null, null); 163 | var task = _buildTask(null, tokenA_canister, #DRC20(#drc20_transferFrom(caller, contract, valueA+tokenFee, null, null, null)), []); 164 | let _tid1 =_getSaga().push(oid, task, null, null); 165 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_transferFrom(to, contract, valueB+tokenFee, null, null, null)), []); 166 | let _tid2 =_getSaga().push(oid, task, null, null); 167 | task := _buildTask(null, Principal.fromActor(this), #custom(#This(#foo(1))), []); 168 | let _tid3 =_getSaga().push(oid, task, null, null); 169 | task := _buildTask(null, tokenA_canister, #DRC20(#drc20_transfer(to, valueA, null, null, null)), []); 170 | let _tid4 =_getSaga().push(oid, task, null, null); 171 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_transfer(caller, valueB, null, null, null)), []); 172 | let _tid5 =_getSaga().push(oid, task, null, null); 173 | _getSaga().close(oid); 174 | let res = await _getSaga().run(oid); 175 | return (oid, res); 176 | }; 177 | 178 | // The callee achieves internal task atomicity 179 | // Caller takes a variety of ways to achieve eventual consistency, including 180 | // ** Retries 181 | // ** Automatic reversal task 182 | // ** Governance or manual reversal task 183 | // Debit first, credit later principle (receive first, freezable txn first) 184 | // Caller-led principle (the caller acts as coordinator) 185 | 186 | public shared(msg) func swap2(_to: Text): async (SagaTM.Toid, ?SagaTM.OrderStatus){ 187 | let valueA: Nat = 100000000; 188 | let valueB: Nat = 200000000; 189 | let tokenFee: Nat = 100000; 190 | let caller: Text = Principal.toText(msg.caller); 191 | let to: Text = _to; 192 | let contract: Text = Principal.toText(Principal.fromActor(this)); 193 | 194 | // Transaction: 195 | // tokenA: transferFrom caller -> contract 1.00000010 // Rollback when an exception occurs 196 | // tokenB: transferFrom to -> contract 2.00000010 // Rollback when an exception occurs 197 | // local: _foo x+100 // Skip when an exception occurs 198 | // tokenA: transfer contract -> to 1.00000000 // Block when an exception occurs 199 | // tokenB: transfer contract -> caller 2.00000000 // Block when an exception occurs 200 | 201 | let oid = _getSaga().create("swap2", #Backward, null, null); 202 | var task = _buildTask(null, tokenA_canister, #DRC20(#drc20_transferFrom(caller, contract, valueA+tokenFee, null, null, null)), []); 203 | var comp = _buildTask(null, tokenA_canister, #DRC20(#drc20_transfer(caller, valueA, null, null, null)), []); 204 | let _tid1 =_getSaga().push(oid, task, ?comp, null); 205 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_transferFrom(to, contract, valueB+tokenFee, null, null, null)), []); 206 | comp := _buildTask(null, tokenB_canister, #DRC20(#drc20_transfer(to, valueB, null, null, null)), []); 207 | let _tid2 =_getSaga().push(oid, task, ?comp, null); 208 | task := _buildTask(null, Principal.fromActor(this), #custom(#This(#foo(1))), []); 209 | comp := _buildTask(null, Principal.fromActor(this), #__skip, []); 210 | let _tid3 =_getSaga().push(oid, task, ?comp, null); 211 | task := _buildTask(null, tokenA_canister, #DRC20(#drc20_transfer(to, valueA, null, null, null)), []); 212 | let _tid4 =_getSaga().push(oid, task, null, null); 213 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_transfer(caller, valueB, null, null, null)), []); 214 | let _tid5 =_getSaga().push(oid, task, null, null); 215 | _getSaga().close(oid); 216 | let res = await _getSaga().run(oid); 217 | return (oid, res); 218 | }; 219 | public shared(msg) func swap3(_to: Text): async (SagaTM.Toid, ?SagaTM.OrderStatus){ 220 | let valueA: Nat = 100000000; 221 | let valueB: Nat = 200000000; 222 | let caller: Text = Principal.toText(msg.caller); 223 | let to: Text = _to; 224 | 225 | // Transaction: 226 | // tokenA: lockTransfer caller -> to 1.00000000 comp: execute 227 | // tokenB: lockTransfer to -> caller 2.00000000 comp: execute 228 | // local: _foo x+100 229 | // tokenA: executeTransfer caller -> to 1.00000000 230 | // tokenB: executeTransfer to -> caller 2.00000000 231 | 232 | let oid = _getSaga().create("swap3", #Backward, null, null); 233 | var task = _buildTask(null, tokenA_canister, #DRC20(#drc20_lockTransferFrom(caller, to, valueA, 100000, null, null, null, null)), []); 234 | var comp = _buildTask(null, tokenA_canister, #DRC20(#drc20_executeTransfer(#AutoFill, #fallback, null, null, null, null)), []); 235 | let tid1 =_getSaga().push(oid, task, ?comp, null); 236 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_lockTransferFrom(to, caller, valueB, 100000, null, null, null, null)), []); 237 | comp := _buildTask(null, tokenB_canister, #DRC20(#drc20_executeTransfer(#AutoFill, #fallback, null, null, null, null)), []); 238 | let tid2 =_getSaga().push(oid, task, ?comp, null); 239 | task := _buildTask(null, Principal.fromActor(this), #custom(#This(#foo(1))), []); 240 | comp := _buildTask(null, Principal.fromActor(this), #__skip, []); 241 | let _tid3 =_getSaga().push(oid, task, null, null); 242 | let _res = await _getSaga().run(oid); 243 | var txid1 : Blob = Blob.fromArray([]); 244 | switch(_getSaga().getActuator().getTaskEvent(tid1)){ 245 | case(?(event)){ 246 | switch(event.result.1){ 247 | case(?(#DRC20(#drc20_executeTransfer(#ok(_txid))))){ txid1 := _txid }; 248 | case(_){}; 249 | }; 250 | }; 251 | case(_){ return (oid, await _getSaga().run(oid)); /* blocking */}; 252 | }; 253 | task := _buildTask(null, tokenA_canister, #DRC20(#drc20_executeTransfer(#ManualFill(txid1), #sendAll, null, null, null, null)), []); 254 | let _tid4 =_getSaga().push(oid, task, null, null); 255 | var txid2 : Blob = Blob.fromArray([]); 256 | switch(_getSaga().getActuator().getTaskEvent(tid2)){ 257 | case(?(event)){ 258 | switch(event.result.1){ 259 | case(?(#DRC20(#drc20_executeTransfer(#ok(_txid))))){ txid2 := _txid }; 260 | case(_){}; 261 | }; 262 | }; 263 | case(_){ return (oid, await _getSaga().run(oid)); /* blocking */}; 264 | }; 265 | task := _buildTask(null, tokenB_canister, #DRC20(#drc20_executeTransfer(#ManualFill(txid2), #sendAll, null, null, null, null)), []); 266 | let _tid5 =_getSaga().push(oid, task, null, null); 267 | _getSaga().close(oid); 268 | return (oid, await _getSaga().run(oid)); 269 | }; 270 | 271 | /** 272 | * ICTC Transaction Explorer Interface 273 | * (Optional) Implement the following interface, which allows you to browse transaction records and execute compensation transactions through a UI interface. 274 | * https://cmqwp-uiaaa-aaaaj-aihzq-cai.raw.ic0.app/ 275 | */ 276 | // ICTC: management functions 277 | private stable var ictc_admins: [Principal] = []; 278 | private func _onlyIctcAdmin(_caller: Principal) : Bool { 279 | return Option.isSome(Array.find(ictc_admins, func (t: Principal): Bool{ t == _caller })); 280 | }; 281 | private func _onlyBlocking(_toid: Nat) : Bool{ 282 | /// Saga 283 | switch(_getSaga().status(_toid)){ 284 | case(?(status)){ return status == #Blocking }; 285 | case(_){ return false; }; 286 | }; 287 | /// 2PC 288 | // switch(_getTPC().status(_toid)){ 289 | // case(?(status)){ return status == #Blocking }; 290 | // case(_){ return false; }; 291 | // }; 292 | }; 293 | public query func ictc_getAdmins() : async [Principal]{ 294 | return ictc_admins; 295 | }; 296 | public shared(msg) func ictc_addAdmin(_admin: Principal) : async (){ 297 | assert(_onlyIctcAdmin(msg.caller)); 298 | if (Option.isNull(Array.find(ictc_admins, func (t: Principal): Bool{ t == _admin }))){ 299 | ictc_admins := TA.arrayAppend(ictc_admins, [_admin]); 300 | }; 301 | }; 302 | public shared(msg) func ictc_removeAdmin(_admin: Principal) : async (){ 303 | assert(_onlyIctcAdmin(msg.caller)); 304 | ictc_admins := Array.filter(ictc_admins, func (t: Principal): Bool{ t != _admin }); 305 | }; 306 | 307 | // SagaTM Scan 308 | public query func ictc_TM() : async Text{ 309 | return "Saga"; 310 | }; 311 | /// Saga 312 | public query func ictc_getTOCount() : async Nat{ 313 | return _getSaga().count(); 314 | }; 315 | public query func ictc_getTO(_toid: SagaTM.Toid) : async ?SagaTM.Order{ 316 | return _getSaga().getOrder(_toid); 317 | }; 318 | public query func ictc_getTOs(_page: Nat, _size: Nat) : async {data: [(SagaTM.Toid, SagaTM.Order)]; totalPage: Nat; total: Nat}{ 319 | return _getSaga().getOrders(_page, _size); 320 | }; 321 | public query func ictc_getPool() : async {toPool: {total: Nat; items: [(SagaTM.Toid, ?SagaTM.Order)]}; ttPool: {total: Nat; items: [(SagaTM.Ttid, Task)]}}{ 322 | let tos = _getSaga().getAliveOrders(); 323 | let tts = _getSaga().getActuator().getTaskPool(); 324 | return { 325 | toPool = { total = tos.size(); items = slice(tos, 0, ?255)}; 326 | ttPool = { total = tts.size(); items = slice(tts, 0, ?255)}; 327 | }; 328 | }; 329 | public query func ictc_getTOPool() : async [(SagaTM.Toid, ?SagaTM.Order)]{ 330 | return _getSaga().getAliveOrders(); 331 | }; 332 | public query func ictc_getTT(_ttid: SagaTM.Ttid) : async ?SagaTM.TaskEvent{ 333 | return _getSaga().getActuator().getTaskEvent(_ttid); 334 | }; 335 | public query func ictc_getTTByTO(_toid: SagaTM.Toid) : async [SagaTM.TaskEvent]{ 336 | return _getSaga().getTaskEvents(_toid); 337 | }; 338 | public query func ictc_getTTs(_page: Nat, _size: Nat) : async {data: [(SagaTM.Ttid, SagaTM.TaskEvent)]; totalPage: Nat; total: Nat}{ 339 | return _getSaga().getActuator().getTaskEvents(_page, _size); 340 | }; 341 | public query func ictc_getTTPool() : async [(SagaTM.Ttid, Task)]{ 342 | return _getSaga().getActuator().getTaskPool(); 343 | }; 344 | public query func ictc_getTTErrors(_page: Nat, _size: Nat) : async {data: [(Nat, SagaTM.ErrorLog)]; totalPage: Nat; total: Nat}{ 345 | return _getSaga().getActuator().getErrorLogs(_page, _size); 346 | }; 347 | public query func ictc_getCalleeStatus(_callee: Principal) : async ?SagaTM.CalleeStatus{ 348 | return _getSaga().getActuator().calleeStatus(_callee); 349 | }; 350 | 351 | // Transaction Governance 352 | public shared(msg) func ictc_clearLog(_expiration: ?Int, _delForced: Bool) : async (){ // Warning: Execute this method with caution 353 | assert(_onlyIctcAdmin(msg.caller)); 354 | _getSaga().clear(_expiration, _delForced); 355 | }; 356 | public shared(msg) func ictc_clearTTPool() : async (){ // Warning: Execute this method with caution 357 | assert(_onlyIctcAdmin(msg.caller)); 358 | _getSaga().getActuator().clearTasks(); 359 | }; 360 | public shared(msg) func ictc_blockTO(_toid: SagaTM.Toid) : async ?SagaTM.Toid{ 361 | assert(_onlyIctcAdmin(msg.caller)); 362 | assert(not(_onlyBlocking(_toid))); 363 | let saga = _getSaga(); 364 | return saga.block(_toid); 365 | }; 366 | // public shared(msg) func ictc_removeTT(_toid: SagaTM.Toid, _ttid: SagaTM.Ttid) : async ?SagaTM.Ttid{ // Warning: Execute this method with caution 367 | // assert(_onlyIctcAdmin(msg.caller)); 368 | // assert(_onlyBlocking(_toid)); 369 | // let saga = _getSaga(); 370 | // saga.open(_toid); 371 | // let ttid = saga.remove(_toid, _ttid); 372 | // saga.close(_toid); 373 | // return ttid; 374 | // }; 375 | public shared(msg) func ictc_appendTT(_businessId: ?Blob, _toid: SagaTM.Toid, _forTtid: ?SagaTM.Ttid, _callee: Principal, _callType: SagaTM.CallType, _preTtids: [SagaTM.Ttid]) : async SagaTM.Ttid{ 376 | // Governance or manual compensation (operation allowed only when a transaction order is in blocking status). 377 | assert(_onlyIctcAdmin(msg.caller)); 378 | assert(_onlyBlocking(_toid)); 379 | let saga = _getSaga(); 380 | saga.open(_toid); 381 | let taskRequest = _buildTask(_businessId, _callee, _callType, _preTtids); 382 | //let ttid = saga.append(_toid, taskRequest, null, null); 383 | let ttid = saga.appendComp(_toid, Option.get(_forTtid, 0), taskRequest, null); 384 | return ttid; 385 | }; 386 | /// Try the task again 387 | public shared(msg) func ictc_redoTT(_toid: SagaTM.Toid, _ttid: SagaTM.Ttid) : async ?SagaTM.Ttid{ 388 | // Warning: proceed with caution! 389 | assert(_onlyIctcAdmin(msg.caller)); 390 | let saga = _getSaga(); 391 | let ttid = saga.redo(_toid, _ttid); 392 | let _r = await saga.run(_toid); 393 | return ttid; 394 | }; 395 | /// set status of pending task 396 | public shared(msg) func ictc_doneTT(_toid: SagaTM.Toid, _ttid: SagaTM.Ttid, _toCallback: Bool) : async ?SagaTM.Ttid{ 397 | // Warning: proceed with caution! 398 | assert(_onlyIctcAdmin(msg.caller)); 399 | let saga = _getSaga(); 400 | try{ 401 | let ttid = await* saga.taskDone(_toid, _ttid, _toCallback); 402 | return ttid; 403 | }catch(e){ 404 | throw Error.reject("420: internal call error: "# Error.message(e)); 405 | }; 406 | }; 407 | /// set status of pending order 408 | public shared(msg) func ictc_doneTO(_toid: SagaTM.Toid, _status: SagaTM.OrderStatus, _toCallback: Bool) : async Bool{ 409 | // Warning: proceed with caution! 410 | assert(_onlyIctcAdmin(msg.caller)); 411 | let saga = _getSaga(); 412 | try{ 413 | let res = await* saga.done(_toid, _status, _toCallback); 414 | return res; 415 | }catch(e){ 416 | throw Error.reject("420: internal call error: "# Error.message(e)); 417 | }; 418 | }; 419 | /// Complete blocking order 420 | public shared(msg) func ictc_completeTO(_toid: SagaTM.Toid, _status: SagaTM.OrderStatus) : async Bool{ 421 | // After governance or manual compensations, this method needs to be called to complete the transaction order. 422 | assert(_onlyIctcAdmin(msg.caller)); 423 | assert(_onlyBlocking(_toid)); 424 | let saga = _getSaga(); 425 | saga.close(_toid); 426 | let _r = await saga.run(_toid); 427 | try{ 428 | let r = await* _getSaga().complete(_toid, _status); 429 | return r; 430 | }catch(e){ 431 | throw Error.reject("430: ICTC error: "# Error.message(e)); 432 | }; 433 | }; 434 | public shared(msg) func ictc_runTO(_toid: SagaTM.Toid) : async ?SagaTM.OrderStatus{ 435 | assert(_onlyIctcAdmin(msg.caller)); 436 | let saga = _getSaga(); 437 | saga.close(_toid); 438 | // await* _ictcSagaRun(_toid, true); 439 | try{ 440 | let r = await saga.run(_toid); 441 | return r; 442 | }catch(e){ 443 | throw Error.reject("430: ICTC error: "# Error.message(e)); 444 | }; 445 | }; 446 | public shared(msg) func ictc_runTT() : async Bool{ 447 | // There is no need to call it normally, but can be called if you want to execute tasks in time when a TO is in the Doing state. 448 | assert(_onlyIctcAdmin(msg.caller)); 449 | let _r = await _getSaga().run(0); 450 | return true; 451 | }; 452 | /** 453 | * End: ICTC Transaction Explorer Interface 454 | */ 455 | 456 | 457 | // upgrade 458 | /// Saga 459 | private stable var __sagaDataNew: ?SagaTM.Data = null; 460 | system func preupgrade() { 461 | let data = _getSaga().getData(); 462 | __sagaDataNew := ?data; 463 | // assert(List.size(data.actuator.tasks.0) == 0 and List.size(data.actuator.tasks.1) == 0); 464 | }; 465 | system func postupgrade() { 466 | switch(__sagaDataNew){ 467 | case(?(data)){ 468 | _getSaga().setData(data); 469 | __sagaDataNew := null; 470 | }; 471 | case(_){}; 472 | }; 473 | }; 474 | 475 | 476 | }; -------------------------------------------------------------------------------- /src/TA.mo: -------------------------------------------------------------------------------- 1 | /** 2 | * Module : TA.mo v3.0 3 | * Author : ICLighthouse Team 4 | * Stability : Experimental 5 | * Description: ICTC Sync Task Actuator. 6 | * Refers : https://github.com/iclighthouse/ICTC 7 | */ 8 | 9 | import Array "mo:base/Array"; 10 | import Blob "mo:base/Blob"; 11 | import Buffer "mo:base/Buffer"; 12 | import CallType "./CallType"; 13 | import Deque "mo:base/Deque"; 14 | import Hash "mo:base/Hash"; 15 | import Iter "mo:base/Iter"; 16 | import List "mo:base/List"; 17 | import Nat "mo:base/Nat"; 18 | import Option "mo:base/Option"; 19 | import Principal "mo:base/Principal"; 20 | import Time "mo:base/Time"; 21 | import TrieMap "mo:base/TrieMap"; 22 | import TATypes "TATypes"; 23 | // import TaskHash "TaskHash"; 24 | import Nat64 "mo:base/Nat64"; 25 | import Binary "mo:icl/Binary"; 26 | import Error "mo:base/Error"; 27 | // import Call "mo:base/ExperimentalInternetComputer"; 28 | 29 | module { 30 | public let Version: Nat = 10; 31 | public type Status = CallType.Status; 32 | public type CallType = CallType.CallType; 33 | public type Receipt = CallType.Receipt; 34 | public type CustomCall = CallType.CustomCall; 35 | public type TaskResult = CallType.TaskResult; 36 | public type Callee = TATypes.Callee; 37 | public type CalleeStatus = TATypes.CalleeStatus; 38 | public type Ttid = TATypes.Ttid; // from 1 39 | public type Toid = TATypes.Toid; // from 1 40 | public type Attempts = TATypes.Attempts; 41 | public type Task = TATypes.Task; 42 | public type AgentCallback = TATypes.AgentCallback; 43 | public type TaskCallback = TATypes.TaskCallback; 44 | public type TaskEvent = TATypes.TaskEvent; 45 | public type ErrorLog = TATypes.ErrorLog; 46 | public type Data = { 47 | tasks: Deque.Deque<(Ttid, Task)>; 48 | taskLogs: [(Ttid, TaskEvent)]; 49 | errorLogs: [(Nat, ErrorLog)]; 50 | callees: [(Callee, CalleeStatus)]; 51 | index: Nat; 52 | firstIndex: Nat; 53 | errIndex: Nat; 54 | firstErrIndex: Nat; 55 | }; 56 | public func arrayAppend(a: [T], b: [T]) : [T]{ 57 | let buffer = Buffer.Buffer(1); 58 | for (t in a.vals()){ 59 | buffer.add(t); 60 | }; 61 | for (t in b.vals()){ 62 | buffer.add(t); 63 | }; 64 | return Buffer.toArray(buffer); 65 | }; 66 | // replace Hash.hash (Warning: Incompatible) 67 | public func natHash(n : Nat) : Hash.Hash{ 68 | return Blob.hash(Blob.fromArray(Binary.BigEndian.fromNat64(Nat64.fromIntWrap(n)))); 69 | }; 70 | public func getTM(_tm: TrieMap.TrieMap, _index: Nat, _firstIndex: Nat, _page: Nat, _size: Nat) : {data: [(Nat, V)]; totalPage: Nat; total: Nat}{ 71 | let length = _tm.size(); 72 | if (_page < 1 or _size < 1){ 73 | return {data = []; totalPage = 0; total = length; }; 74 | }; 75 | let offset = Nat.sub(_page, 1) * _size; 76 | var start: Nat = 0; 77 | var end: Nat = 0; 78 | if (offset < Nat.sub(_index,1)){ 79 | start := Nat.sub(Nat.sub(_index,1), offset); 80 | if (start < _firstIndex){ 81 | return {data = []; totalPage = 0; total = length; }; 82 | }; 83 | if (start > Nat.sub(_size, 1)){ 84 | end := Nat.max(Nat.sub(start, Nat.sub(_size, 1)), _firstIndex); 85 | }else{ 86 | end := _firstIndex; 87 | }; 88 | }else{ 89 | return {data = []; totalPage = 0; total = length; }; 90 | }; 91 | var data: [(Nat, V)] = []; 92 | for (i in Iter.range(end, start)){ 93 | switch(_tm.get(i)){ 94 | case(?(item)){ 95 | data := arrayAppend(data, [(i, item)]); 96 | }; 97 | case(_){}; 98 | }; 99 | }; 100 | var totalPage: Nat = length / _size; 101 | if (totalPage * _size < length) { totalPage += 1; }; 102 | return {data = data; totalPage = totalPage; total = length; }; 103 | }; 104 | 105 | /// limitNum: The actuator runs `limitNum` tasks at once. 106 | public class TA(limitNum: Nat, autoClearTimeout: Int, call: CustomCall, agentCallback: ?AgentCallback, taskCallback: ?TaskCallback) { 107 | var tasks: Deque.Deque<(Ttid, Task)> = Deque.empty<(Ttid, Task)>(); /*fix*/ 108 | var index : Nat = 1; 109 | var firstIndex : Nat = 1; 110 | var taskLogs = TrieMap.TrieMap> (Nat.equal, natHash); /*fix*/ 111 | var errIndex : Nat = 1; 112 | var firstErrIndex : Nat = 1; 113 | var errorLogs = TrieMap.TrieMap (Nat.equal, natHash); /*fix*/ 114 | var callees = TrieMap.TrieMap (Principal.equal, Principal.hash); 115 | var actuationThreads : Nat = 0; 116 | var lastActuationTime : Time.Time = 0; 117 | var countAsyncMessage : Nat = 0; 118 | // var receiption : ?CallType.Receipt = null; 119 | // public func getReceiption() : ?CallType.Receipt{ 120 | // return receiption; 121 | // }; 122 | 123 | private func _push(_ttid: Ttid, _task: Task) : () { 124 | tasks := Deque.pushBack(tasks, (_ttid, _task)); 125 | }; 126 | private func _update(_ttid: Ttid, _task: Task) : () { 127 | assert(_ttid < index); 128 | ignore _remove(_ttid); 129 | _push(_ttid, _task); 130 | }; 131 | private func _remove(_ttid: Ttid) : ?Ttid{ 132 | let length: Nat = _size(); 133 | if (length == 0){ return null; }; 134 | var res: ?Ttid = null; 135 | for(i in Iter.range(1, length)){ 136 | switch(Deque.popFront(tasks)){ 137 | case(?((ttid, task), deque)){ 138 | tasks := deque; 139 | if (_ttid != ttid){ 140 | _push(ttid, task); 141 | }else{ 142 | res := ?_ttid; 143 | }; 144 | }; 145 | case(_){}; 146 | }; 147 | }; 148 | return res; 149 | }; 150 | private func _removeByOid(_toid: Toid) : [Ttid]{ 151 | let length: Nat = _size(); 152 | if (length == 0){ return []; }; 153 | var res: [Ttid] = []; 154 | for(i in Iter.range(1, length)){ 155 | switch(Deque.popFront(tasks)){ 156 | case(?((ttid, task), deque)){ 157 | tasks := deque; 158 | if (_toid != Option.get(task.toid, 0)){ 159 | _push(ttid, task); 160 | }else{ 161 | res := arrayAppend(res, [ttid]); 162 | }; 163 | }; 164 | case(_){}; 165 | }; 166 | }; 167 | return res; 168 | }; 169 | private func _size() : Nat { 170 | return List.size(tasks.0) + List.size(tasks.1); 171 | }; 172 | private func _toArray() : [(Ttid, Task)] { 173 | return arrayAppend(List.toArray(tasks.0), List.toArray(tasks.1)); 174 | }; 175 | private func _filter(_ttid: Ttid, _task: Task) : Bool { 176 | for (preTtid in _task.preTtid.vals()){ 177 | if (preTtid > 0){ 178 | switch(taskLogs.get(preTtid)){ 179 | case(?(taskLog)){ 180 | if (taskLog.result.0 != #Done){ return false; }; 181 | }; 182 | case(_){ return false; }; 183 | }; 184 | }; 185 | }; 186 | switch(taskLogs.get(_ttid)){ 187 | case(?(taskLog)){ 188 | // if (taskLog.attempts+1 > _task.attemptsMax){ 189 | // return false; 190 | // }; 191 | if (Time.now() < taskLog.time + _task.recallInterval){ 192 | return false; 193 | }; 194 | }; 195 | case(_){}; 196 | }; 197 | return true; 198 | }; 199 | private func _preLog(_ttid: Ttid, _task: Task) : Attempts{ 200 | var attempts: Attempts = 1; 201 | switch(taskLogs.get(_ttid)){ 202 | case(?(taskLog)){ 203 | attempts := taskLog.attempts + 1; 204 | }; 205 | case(_){}; 206 | }; 207 | let taskLog: TaskEvent = { 208 | toid = _task.toid; 209 | ttid = _ttid; 210 | task = _task; 211 | attempts = attempts; 212 | result = (#Doing, null, null); 213 | callbackStatus = ?#Todo; 214 | time = Time.now(); 215 | txHash = Blob.fromArray([]); 216 | }; 217 | taskLogs.put(_ttid, taskLog); 218 | return attempts; 219 | }; 220 | private func _postLog(_ttid: Ttid, _result: TaskResult) : (){ 221 | switch(taskLogs.get(_ttid)){ 222 | case(?(taskLog)){ 223 | var log: TaskEvent = { 224 | toid = taskLog.toid; 225 | ttid = taskLog.ttid; 226 | task = taskLog.task; 227 | attempts = taskLog.attempts; 228 | result = _result; 229 | callbackStatus = null; 230 | time = Time.now(); 231 | txHash = taskLog.txHash; 232 | }; 233 | // let txHash = TaskHash.hashb(Blob.fromArray([]), log); 234 | // log := { 235 | // toid = log.toid; 236 | // ttid = log.ttid; 237 | // task = log.task; 238 | // attempts = log.attempts; 239 | // result = log.result; 240 | // callbackStatus = log.callbackStatus; 241 | // time = log.time; 242 | // txHash = txHash; 243 | // }; 244 | taskLogs.put(_ttid, log); 245 | var calleeStatus = { successCount = 1; failureCount = 0; continuousFailure = 0;}; 246 | if (_result.0 == #Error or _result.0 == #Unknown){ 247 | calleeStatus := { successCount = 0; failureCount = 1; continuousFailure = 1;}; 248 | let errLog = { ttid = _ttid; callee = ?taskLog.task.callee; result = ?_result; time = Time.now(); }; 249 | errorLogs.put(errIndex, errLog); 250 | errIndex += 1; 251 | }; 252 | switch(callees.get(taskLog.task.callee)){ 253 | case(?(status)){ 254 | var successCount: Nat = status.successCount; 255 | var failureCount: Nat = status.failureCount; 256 | var continuousFailure: Nat = status.continuousFailure; 257 | if (_result.0 == #Error or _result.0 == #Unknown){ 258 | failureCount += 1; 259 | continuousFailure += 1; 260 | }else{ 261 | successCount += 1; 262 | continuousFailure := 0; 263 | }; 264 | calleeStatus := { 265 | successCount = successCount; 266 | failureCount = failureCount; 267 | continuousFailure = continuousFailure; 268 | }; 269 | }; 270 | case(_){}; 271 | }; 272 | callees.put(taskLog.task.callee, calleeStatus); 273 | }; 274 | case(_){ 275 | let errLog = { ttid = _ttid; callee = null; result = null; time = Time.now(); }; 276 | errorLogs.put(errIndex, errLog); 277 | errIndex += 1; 278 | }; 279 | }; 280 | }; 281 | private func _callbackLog(_ttid: Ttid, _callbackStatus: ?Status) : (){ 282 | switch(taskLogs.get(_ttid)){ 283 | case(?(taskLog)){ 284 | let log: TaskEvent = { 285 | toid = taskLog.toid; 286 | ttid = taskLog.ttid; 287 | task = taskLog.task; 288 | attempts = taskLog.attempts; 289 | result = taskLog.result; 290 | callbackStatus = _callbackStatus; 291 | time = taskLog.time; 292 | txHash = taskLog.txHash; 293 | }; 294 | taskLogs.put(_ttid, log); 295 | }; 296 | case(_){}; 297 | }; 298 | }; 299 | private func _clear(_expiration: ?Int, _clearErr: Bool) : (){ 300 | var clearTimeout: Int = Option.get(_expiration, 0); 301 | if (clearTimeout == 0 and autoClearTimeout > 0){ 302 | clearTimeout := autoClearTimeout; 303 | }else if(clearTimeout == 0){ 304 | return (); 305 | }; 306 | var completed: Bool = false; 307 | var moveFirstPointer: Bool = true; 308 | var i: Nat = firstIndex; 309 | while (i < index and not(completed)){ 310 | switch(taskLogs.get(i)){ 311 | case(?(taskLog)){ 312 | if (Time.now() > taskLog.time + clearTimeout and taskLog.result.0 != #Todo and taskLog.result.0 != #Doing){ 313 | taskLogs.delete(i); 314 | i += 1; 315 | }else if (Time.now() > taskLog.time + clearTimeout){ 316 | i += 1; 317 | moveFirstPointer := false; 318 | }else{ 319 | moveFirstPointer := false; 320 | completed := true; 321 | }; 322 | }; 323 | case(_){ 324 | i += 1; 325 | }; 326 | }; 327 | if (moveFirstPointer) { firstIndex += 1; }; 328 | }; 329 | if (_clearErr){ 330 | completed := false; 331 | while (firstErrIndex < errIndex and not(completed)){ 332 | switch(errorLogs.get(firstErrIndex)){ 333 | case(?(taskLog)){ 334 | if (Time.now() > taskLog.time + clearTimeout){ 335 | errorLogs.delete(firstErrIndex); 336 | firstErrIndex += 1; 337 | }else{ 338 | completed := true; 339 | }; 340 | }; 341 | case(_){ 342 | firstErrIndex += 1; 343 | }; 344 | }; 345 | }; 346 | }; 347 | }; 348 | // private func _run() : async Nat { 349 | // }; 350 | 351 | public func done(_ttid: Ttid, _toCallback: Bool) : async* ?Ttid{ 352 | var status: Status = #Done; 353 | switch(taskLogs.get(_ttid)){ 354 | case(?(log)){ 355 | if (log.result.0 != #Done){ 356 | if (_toCallback){ 357 | var callbackStatus: ?Status = null; 358 | switch(agentCallback){ 359 | case(?(_agentCallback)){ 360 | try{ 361 | await* _agentCallback(_ttid, log.task, (#Done, null, null)); 362 | callbackStatus := ?#Done; 363 | } catch(e){ 364 | callbackStatus := ?#Error; 365 | status := #Error; 366 | }; 367 | }; 368 | case(_){ 369 | switch(taskCallback){ 370 | case(?(_taskCallback)){ 371 | try{ 372 | countAsyncMessage += 2; 373 | await _taskCallback("", _ttid, log.task, (#Done, null, null)); 374 | callbackStatus := ?#Done; 375 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 376 | } catch(e){ 377 | callbackStatus := ?#Error; 378 | status := #Error; 379 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 380 | }; 381 | }; 382 | case(_){}; 383 | }; 384 | }; 385 | }; 386 | _callbackLog(_ttid, callbackStatus); 387 | }; 388 | _postLog(_ttid, (status, null, null)); 389 | return ?_ttid; 390 | } else { 391 | return null; 392 | }; 393 | }; 394 | case(_){ return null; }; 395 | }; 396 | }; 397 | 398 | public func update(_ttid: Ttid, _task: Task) : Ttid { 399 | assert(_ttid > 0 and _ttid < index); 400 | _update(_ttid, _task); 401 | return _ttid; 402 | }; 403 | public func push(_task: Task) : Ttid { 404 | var ttid = index; 405 | index += 1; 406 | _push(ttid, _task); 407 | return ttid; 408 | }; 409 | public func remove(_ttid: Ttid) : ?Ttid{ 410 | return _remove(_ttid); 411 | }; 412 | public func removeByOid(_toid: Toid) : [Ttid]{ 413 | return _removeByOid(_toid); 414 | }; 415 | public func run() : async* Nat{ 416 | return await* runSync(null); 417 | }; 418 | public func runSync(_ttids: ?[Ttid]) : async* Nat{ 419 | var size: Nat = _size(); 420 | var count: Nat = 0; 421 | var callCount: Nat = 0; 422 | var receipt: ?Receipt = null; 423 | var ttids: [Ttid] = Option.get(_ttids, []); 424 | actuationThreads += 1; 425 | while (count < (if (ttids.size() == 0){ limitNum }else{ limitNum * 10 }) and callCount < size * 5 and Option.isSome(Deque.peekFront(tasks))){ 426 | lastActuationTime := Time.now(); 427 | switch(Deque.popFront(tasks)){ 428 | case(?((ttid, task_), deque)){ 429 | tasks := deque; 430 | var task: Task = task_; 431 | var toRedo: Bool = true; 432 | if(_filter(ttid, task) and (_ttids == null or Option.isSome(Array.find(ttids, func (t: Ttid): Bool{ t == ttid })))){ 433 | //get receipt 434 | switch(task.forTtid){ 435 | case(?(forTtid)){ 436 | switch(taskLogs.get(forTtid)){ 437 | case(?(taskLog)){ 438 | receipt := taskLog.result.1; 439 | }; 440 | case(_){}; 441 | }; 442 | }; 443 | case(_){}; 444 | }; 445 | //receiption := receipt; // for test 446 | //prelog 447 | var attempts = _preLog(ttid, task); // attempts+1, set #Doing 448 | //call 449 | var result : TaskResult = (#Doing, null,null); 450 | try{ 451 | countAsyncMessage += 3; 452 | result := await* CallType.call(task.callee, task.cycles, call, task.callType, receipt); // (Status, ?Receipt, ?Err) 453 | countAsyncMessage -= Nat.min(3, countAsyncMessage); 454 | }catch(e){ 455 | result := (#Error, null, ?{code = Error.code(e); message = Error.message(e); }); 456 | countAsyncMessage -= Nat.min(3, countAsyncMessage); 457 | }; 458 | lastActuationTime := Time.now(); 459 | var callbackStatus: ?Status = null; 460 | var status: Status = result.0; 461 | var errorMsg: ?CallType.Err = result.2; 462 | if (status == #Done or status == #Unknown or attempts >= task.attemptsMax){ 463 | //callback 464 | switch(agentCallback){ 465 | case(?(_agentCallback)){ 466 | try{ 467 | await* _agentCallback(ttid, task, result); 468 | callbackStatus := ?#Done; 469 | } catch(e){ 470 | callbackStatus := ?#Error; 471 | status := #Error; 472 | errorMsg := ?{code = Error.code(e); message = Error.message(e); }; 473 | }; 474 | lastActuationTime := Time.now(); 475 | }; 476 | case(_){ 477 | switch(taskCallback){ 478 | case(?(_taskCallback)){ 479 | try{ 480 | countAsyncMessage += 2; 481 | await _taskCallback("", ttid, task, result); 482 | callbackStatus := ?#Done; 483 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 484 | } catch(e){ 485 | callbackStatus := ?#Error; 486 | status := #Error; 487 | errorMsg := ?{code = Error.code(e); message = Error.message(e); }; 488 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 489 | }; 490 | lastActuationTime := Time.now(); 491 | }; 492 | case(_){}; 493 | }; 494 | }; 495 | }; 496 | //postlog 497 | result := (status, result.1, errorMsg); 498 | _postLog(ttid, result); 499 | //callbacklog 500 | _callbackLog(ttid, callbackStatus); 501 | toRedo := false; 502 | }else if (attempts < task.attemptsMax){ 503 | switch(taskLogs.get(ttid)){ 504 | case(?(taskLog)){ 505 | var log: TaskEvent = { 506 | toid = taskLog.toid; 507 | ttid = taskLog.ttid; 508 | task = taskLog.task; 509 | attempts = taskLog.attempts; 510 | result = (taskLog.result.0, result.1, errorMsg); 511 | callbackStatus = taskLog.callbackStatus; 512 | time = taskLog.time; 513 | txHash = taskLog.txHash; 514 | }; 515 | taskLogs.put(ttid, log); 516 | }; 517 | case(_){}; 518 | }; 519 | toRedo := true; 520 | }; 521 | callCount += 1; 522 | }; 523 | //redo 524 | if (toRedo){ 525 | _push(ttid, task); 526 | }; 527 | //autoClear 528 | _clear(null, false); 529 | count += 1; 530 | }; 531 | case(_){}; 532 | }; 533 | }; 534 | actuationThreads := 0; 535 | return callCount; 536 | }; 537 | public func clear(_expiration: ?Int, _clearErr: Bool) : (){ 538 | _clear(_expiration, _clearErr); 539 | }; 540 | public func clearTasks() : (){ 541 | tasks := Deque.empty<(Ttid, Task)>(); 542 | }; 543 | 544 | public func isInPool(_ttid: Ttid) : Bool{ 545 | return Option.isSome(Array.find(_toArray(), func (item: (Ttid, Task)): Bool{ _ttid == item.0 })); 546 | }; 547 | public func getTaskPool() : [(Ttid, Task)]{ 548 | return _toArray(); 549 | }; 550 | public func getSize() : Nat{ 551 | return _size(); 552 | }; 553 | public func actuations() : {actuationThreads: Nat; lastActuationTime: Time.Time; countAsyncMessage: Nat}{ 554 | return {actuationThreads = actuationThreads; lastActuationTime = lastActuationTime; countAsyncMessage = countAsyncMessage }; 555 | }; 556 | 557 | public func isCompleted(_ttid: Ttid) : Bool{ 558 | switch(taskLogs.get(_ttid)){ 559 | case(?(log)){ 560 | if (log.result.0 == #Done){ 561 | return true; 562 | } else { 563 | return false; 564 | }; 565 | }; 566 | case(_){ return false; }; 567 | }; 568 | }; 569 | public func getTaskEvent(_ttid: Ttid) : ?TaskEvent{ 570 | return taskLogs.get(_ttid); 571 | }; 572 | 573 | public func getTaskEvents(_page: Nat, _size: Nat) : {data: [(Ttid, TaskEvent)]; totalPage: Nat; total: Nat}{ 574 | return getTM>(taskLogs, index, firstIndex, _page, _size); 575 | }; 576 | public func getErrorLogs(_page: Nat, _size: Nat) : {data: [(Nat, ErrorLog)]; totalPage: Nat; total: Nat}{ 577 | return getTM(errorLogs, errIndex, firstErrIndex, _page, _size); 578 | }; 579 | public func calleeStatus(_callee: Callee) : ?CalleeStatus{ 580 | return callees.get(_callee); 581 | }; 582 | 583 | public func getData() : Data { 584 | return { 585 | tasks = tasks; 586 | taskLogs = Iter.toArray(taskLogs.entries()); 587 | errorLogs = Iter.toArray(errorLogs.entries()); 588 | callees = Iter.toArray(callees.entries()); 589 | index = index; 590 | firstIndex = firstIndex; 591 | errIndex = errIndex; 592 | firstErrIndex = firstErrIndex; 593 | }; 594 | }; 595 | public func getDataBase() : Data { 596 | let _taskLogs = Iter.toArray(Iter.filter(taskLogs.entries(), func (x: (Ttid, TaskEvent)): Bool{ 597 | x.1.time + 96*3600*1000000000 > Time.now() or x.1.result.0 == #Todo or x.1.result.0 == #Doing or 598 | List.some(Iter.toList(errorLogs.vals()), func (v: ErrorLog): Bool{ x.0 == v.ttid }) 599 | })); 600 | return { 601 | tasks = tasks; 602 | taskLogs = _taskLogs; 603 | errorLogs = Iter.toArray(errorLogs.entries()); 604 | callees = Iter.toArray(callees.entries()); 605 | index = index; 606 | firstIndex = firstIndex; 607 | errIndex = errIndex; 608 | firstErrIndex = firstErrIndex; 609 | }; 610 | }; 611 | public func setData(_data: Data) : (){ 612 | tasks := _data.tasks; 613 | taskLogs := TrieMap.fromEntries(_data.taskLogs.vals(), Nat.equal, natHash); 614 | errorLogs := TrieMap.fromEntries(_data.errorLogs.vals(), Nat.equal, natHash); 615 | callees := TrieMap.fromEntries(_data.callees.vals(), Principal.equal, Principal.hash); 616 | index := _data.index; 617 | firstIndex := _data.firstIndex; 618 | errIndex := _data.errIndex; 619 | firstErrIndex := _data.firstErrIndex; 620 | }; 621 | 622 | }; 623 | 624 | 625 | }; -------------------------------------------------------------------------------- /src/SagaTM.mo: -------------------------------------------------------------------------------- 1 | /** 2 | * Module : SagaTM.mo v3.0 3 | * Author : ICLighthouse Team 4 | * Stability : Experimental 5 | * Description: ICTC Saga Transaction Manager. 6 | * Refers : https://github.com/iclighthouse/ICTC 7 | */ 8 | 9 | import Nat "mo:base/Nat"; 10 | import Array "mo:base/Array"; 11 | import Option "mo:base/Option"; 12 | import Principal "mo:base/Principal"; 13 | import Time "mo:base/Time"; 14 | import Iter "mo:base/Iter"; 15 | import List "mo:base/List"; 16 | import TrieMap "mo:base/TrieMap"; 17 | import TA "./TA"; 18 | import Error "mo:base/Error"; 19 | 20 | module { 21 | public let Version: Nat = 10; 22 | public type Toid = Nat; 23 | public type Ttid = TA.Ttid; 24 | public type Tcid = TA.Ttid; 25 | public type Callee = TA.Callee; 26 | public type CallType = TA.CallType; 27 | public type Receipt = TA.Receipt; 28 | public type Task = TA.Task; 29 | public type Status = TA.Status; 30 | public type TaskCallback = TA.TaskCallback; 31 | public type CustomCall = TA.CustomCall; 32 | public type TaskResult = TA.TaskResult; 33 | public type TaskEvent = TA.TaskEvent; 34 | public type ErrorLog = TA.ErrorLog; 35 | public type CalleeStatus = TA.CalleeStatus; 36 | public type Settings = {attemptsMax: ?TA.Attempts; recallInterval: ?Int; data: ?Blob}; 37 | public type OrderStatus = {#Todo; #Doing; #Compensating; #Blocking; #Done; #Recovered;}; 38 | public type Compensation = Task; 39 | public type CompStrategy = { #Forward; #Backward; }; 40 | public type OrderCallback = (_toName: Text, _toid: Toid, _status: OrderStatus, _data: ?Blob) -> async (); 41 | public type PushTaskRequest = { 42 | callee: Callee; 43 | callType: CallType; 44 | preTtid: [Ttid]; 45 | attemptsMax: ?Nat; 46 | recallInterval: ?Int; // nanoseconds 47 | cycles: Nat; 48 | data: ?Blob; 49 | }; 50 | public type PushCompRequest = PushTaskRequest; 51 | public type SagaTask = { 52 | ttid: Ttid; 53 | task: Task; 54 | comp: ?Compensation; // for auto compensation 55 | status: Status; 56 | }; 57 | public type CompTask = { 58 | forTtid: Ttid; 59 | tcid: Tcid; 60 | comp: Compensation; 61 | status: Status; 62 | }; 63 | public type Order = { 64 | name: Text; 65 | compStrategy: CompStrategy; 66 | tasks: List.List>; 67 | allowPushing: {#Opening; #Closed;}; 68 | comps: List.List>; 69 | status: OrderStatus; // * 70 | callbackStatus: ?Status; 71 | time: Time.Time; 72 | data: ?Blob; 73 | }; 74 | public type Data = { 75 | autoClearTimeout: Int; 76 | index: Nat; 77 | firstIndex: Nat; 78 | orders: [(Toid, Order)]; 79 | aliveOrders: List.List<(Toid, Time.Time)>; 80 | taskEvents: [(Toid, [Ttid])]; 81 | actuator: TA.Data; 82 | }; 83 | 84 | /// ## Transaction Manager for Saga mode. 85 | /// - Transaction Order: is a complete transaction containing one or more tasks. 86 | /// - Transaction Task: is a task within a transaction that is required to be data consistent internally (atomicity) and 87 | /// preferably acceptable for multiple attempts without repeated execution (idempotence). 88 | public class SagaTM(this: Principal, call: ?CustomCall, defaultTaskCallback: ?TaskCallback, defaultOrderCallback: ?OrderCallback) { 89 | let limitAtOnce: Nat = 500; 90 | var autoClearTimeout: Int = 3*30*24*3600*1000000000; // 3 months 91 | var index: Toid = 1; 92 | var firstIndex: Toid = 1; 93 | var orders = TrieMap.TrieMap>(Nat.equal, TA.natHash); 94 | var aliveOrders = List.nil<(Toid, Time.Time)>(); 95 | var taskEvents = TrieMap.TrieMap(Nat.equal, TA.natHash); 96 | var actuator_: ?TA.TA = null; 97 | var taskCallback = TrieMap.TrieMap>(Nat.equal, TA.natHash); /*fix*/ 98 | var orderCallback = TrieMap.TrieMap(Nat.equal, TA.natHash); 99 | var countAsyncMessage : Nat = 0; 100 | 101 | private func actuator() : TA.TA { 102 | switch(actuator_){ 103 | case(?(_actuator)){ return _actuator; }; 104 | case(_){ 105 | let call_ = Option.get(call, func (_callee: Principal, _cycles: Nat, _ct: CallType, _r: ?Receipt): async (TaskResult){ (#Error, null, ?{code = #future(9902); message = "No custom calling function proxy specified"; }) }); 106 | let act = TA.TA(limitAtOnce, autoClearTimeout, call_, ?_taskCallbackProxy, null); 107 | actuator_ := ?act; 108 | return act; 109 | }; 110 | }; 111 | }; 112 | 113 | // Unique callback entrance. This function will call each specified callback of task 114 | private func _taskCallbackProxy(_ttid: Ttid, _task: Task, _result: TaskResult) : async* (){ 115 | let toid = Option.get(_task.toid, 0); 116 | switch(_status(toid)){ 117 | case(?(#Todo)){ _setStatus(toid, #Doing); }; 118 | case(_){}; 119 | }; 120 | var orderStatus : OrderStatus = #Todo; 121 | var strategy: CompStrategy = #Backward; 122 | var isClosed : Bool = false; 123 | var toName : Text = ""; 124 | switch(orders.get(toid)){ 125 | case(?(order)){ 126 | orderStatus := order.status; 127 | strategy := order.compStrategy; 128 | isClosed := order.allowPushing == #Closed; 129 | toName := order.name; 130 | }; 131 | case(_){}; 132 | }; 133 | // task status 134 | ignore _setTaskStatus(toid, _ttid, _result.0); 135 | // task callback 136 | var callbackDone : Bool = false; 137 | switch(taskCallback.get(_ttid)){ 138 | case(?(_taskCallback)){ 139 | try{ 140 | countAsyncMessage += 2; 141 | await _taskCallback(toName, _ttid, _task, _result); 142 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 143 | taskCallback.delete(_ttid); 144 | callbackDone := true; 145 | } catch(e){ 146 | callbackDone := false; 147 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 148 | }; 149 | }; 150 | case(_){ 151 | switch(defaultTaskCallback){ 152 | case(?(_taskCallback)){ 153 | try{ 154 | countAsyncMessage += 2; 155 | await _taskCallback(toName, _ttid, _task, _result); 156 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 157 | callbackDone := true; 158 | }catch(e){ 159 | callbackDone := false; 160 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 161 | }; 162 | }; 163 | case(_){ callbackDone := true; }; 164 | }; 165 | }; 166 | }; 167 | // process 168 | if (orderStatus == #Compensating){ //Compensating 169 | if (_result.0 == #Done and isClosed and Option.get(_orderLastCid(toid), 0) == _ttid){ 170 | _setStatus(toid, #Recovered); 171 | var callbackStatus : ?Status = null; 172 | try{ 173 | callbackStatus := await* _orderComplete(toid); 174 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != toid }); 175 | }catch(e){ 176 | callbackStatus := ?#Error; 177 | _setStatus(toid, #Blocking); 178 | }; 179 | _setCallbackStatus(toid, callbackStatus); 180 | //await _orderComplete(toid, #Recovered); 181 | _removeTATaskByOid(toid); 182 | }else if (_result.0 == #Error or _result.0 == #Unknown or not(callbackDone)){ //Blocking 183 | _setStatus(toid, #Blocking); 184 | }; 185 | } else if (orderStatus == #Doing){ //Doing 186 | if (_result.0 == #Done and isClosed and Option.get(_orderLastTid(toid), 0) == _ttid){ // Done 187 | _setStatus(toid, #Done); 188 | var callbackStatus : ?Status = null; 189 | try{ 190 | callbackStatus := await* _orderComplete(toid); 191 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != toid }); 192 | }catch(e){ 193 | callbackStatus := ?#Error; 194 | _setStatus(toid, #Blocking); 195 | }; 196 | _setCallbackStatus(toid, callbackStatus); 197 | //await _orderComplete(toid, #Done); 198 | }else if (_result.0 == #Error and strategy == #Backward){ // recovery 199 | _setStatus(toid, #Compensating); 200 | _compensate(toid, _ttid); 201 | }else if (_result.0 == #Error or _result.0 == #Unknown or not(callbackDone)){ //Blocking 202 | _setStatus(toid, #Blocking); 203 | }; 204 | } else { // Blocking 205 | // 206 | }; 207 | //taskEvents 208 | switch(taskEvents.get(toid)){ 209 | case(?(events)){ 210 | taskEvents.put(toid, TA.arrayAppend(events, [_ttid])); 211 | }; 212 | case(_){ 213 | taskEvents.put(toid, [_ttid]); 214 | }; 215 | }; 216 | //return 217 | if (not(callbackDone)){ 218 | throw Error.reject("Task Callback Error."); 219 | }; 220 | }; 221 | 222 | 223 | // private functions 224 | private func _inOrders(_toid: Toid): Bool{ 225 | return Option.isSome(orders.get(_toid)); 226 | }; 227 | private func _inAliveOrders(_toid: Toid): Bool{ 228 | return Option.isSome(List.find(aliveOrders, func (item: (Toid, Time.Time)): Bool{ item.0 == _toid })); 229 | }; 230 | private func _pushOrder(_toid: Toid, _order: Order): (){ 231 | orders.put(_toid, _order); 232 | _clear(null, false); 233 | }; 234 | private func _clear(_expiration: ?Int, _delForced: Bool) : (){ 235 | var clearTimeout: Int = Option.get(_expiration, 0); 236 | if (clearTimeout == 0 and autoClearTimeout > 0){ 237 | clearTimeout := autoClearTimeout; 238 | }else if(clearTimeout == 0){ 239 | return (); 240 | }; 241 | var completed: Bool = false; 242 | var moveFirstPointer: Bool = true; 243 | var i: Nat = firstIndex; 244 | while (i < index and not(completed)){ 245 | switch(orders.get(i)){ 246 | case(?(order)){ 247 | if (Time.now() > order.time + clearTimeout and (_delForced or order.status == #Done or order.status == #Recovered)){ 248 | _deleteOrder(i); // delete the record. 249 | i += 1; 250 | }else if (Time.now() > order.time + clearTimeout){ 251 | i += 1; 252 | moveFirstPointer := false; 253 | }else{ 254 | moveFirstPointer := false; 255 | completed := true; 256 | }; 257 | }; 258 | case(_){ 259 | i += 1; 260 | }; 261 | }; 262 | if (moveFirstPointer) { firstIndex += 1; }; 263 | }; 264 | }; 265 | private func _deleteOrder(_toid: Toid) : (){ 266 | switch(orders.get(_toid)){ 267 | case(?(order)){ 268 | orders.delete(_toid); 269 | taskEvents.delete(_toid); 270 | }; 271 | case(_){}; 272 | }; 273 | }; 274 | 275 | private func _taskFromRequest(_toid: Toid, _task: PushTaskRequest, _autoAddPreTtid: Bool) : TA.Task{ 276 | var preTtid = _task.preTtid; 277 | let lastTtid = Option.get(_orderLastTid(_toid), 0); 278 | if (_autoAddPreTtid and Option.isNull(Array.find(preTtid, func (ttid:Ttid):Bool{ttid == lastTtid})) and lastTtid > 0){ 279 | preTtid := TA.arrayAppend(preTtid, [lastTtid]); 280 | }; 281 | return { 282 | callee = _task.callee; 283 | callType = _task.callType; 284 | preTtid = preTtid; 285 | toid = ?_toid; 286 | forTtid = null; 287 | attemptsMax = Option.get(_task.attemptsMax, 1); 288 | recallInterval = Option.get(_task.recallInterval, 0); 289 | cycles = _task.cycles; 290 | data = _task.data; 291 | time = Time.now(); 292 | }; 293 | }; 294 | private func _compFromRequest(_toid: Toid, _forTtid: ?Ttid, _comp: ?PushCompRequest) : ?Compensation{ 295 | var comp: ?Compensation = null; 296 | switch(_comp){ 297 | case(?(compensation)){ 298 | comp := ?{ 299 | callee = compensation.callee; 300 | callType = compensation.callType; 301 | preTtid = []; 302 | toid = ?_toid; 303 | forTtid = _forTtid; 304 | attemptsMax = Option.get(compensation.attemptsMax, 1); 305 | recallInterval = Option.get(compensation.recallInterval, 0); 306 | cycles = compensation.cycles; 307 | data = compensation.data; 308 | time = Time.now(); 309 | }; 310 | }; 311 | case(_){}; 312 | }; 313 | return comp; 314 | }; 315 | private func _orderLastTid(_toid: Toid) : ?Ttid{ 316 | switch(orders.get(_toid)){ 317 | case(?(order)){ 318 | switch(List.pop(order.tasks)){ 319 | case((?(task), ts)){ 320 | return ?task.ttid; 321 | }; 322 | case(_){ return null; }; 323 | }; 324 | }; 325 | case(_){ return null; }; 326 | }; 327 | }; 328 | private func _putTask(_toid: Toid, _sagaTask: SagaTask) : (){ 329 | switch(orders.get(_toid)){ 330 | case(?(order)){ 331 | assert(order.allowPushing == #Opening); 332 | let tasks = List.push(_sagaTask, order.tasks); 333 | let orderNew = { 334 | name = order.name; 335 | compStrategy = order.compStrategy; 336 | tasks = tasks; 337 | allowPushing = order.allowPushing; 338 | comps = order.comps; 339 | status = order.status; 340 | callbackStatus = order.callbackStatus; 341 | time = order.time; 342 | data = order.data; 343 | }; 344 | orders.put(_toid, orderNew); 345 | }; 346 | case(_){}; 347 | }; 348 | if (_toid > 0 and not(_inAliveOrders(_toid))){ 349 | aliveOrders := List.push((_toid, Time.now()), aliveOrders); 350 | }; 351 | }; 352 | private func _updateTask(_toid: Toid, _sagaTask: SagaTask) : (){ 353 | switch(orders.get(_toid)){ 354 | case(?(order)){ 355 | let tasks = List.map(order.tasks, func (t:SagaTask):SagaTask{ 356 | if (t.ttid == _sagaTask.ttid){ _sagaTask } else { t }; 357 | }); 358 | let orderNew = { 359 | name = order.name; 360 | compStrategy = order.compStrategy; 361 | tasks = tasks; 362 | allowPushing = order.allowPushing; 363 | comps = order.comps; 364 | status = order.status; 365 | callbackStatus = order.callbackStatus; 366 | time = order.time; 367 | data = order.data; 368 | }; 369 | orders.put(_toid, orderNew); 370 | }; 371 | case(_){}; 372 | }; 373 | if (_toid > 0 and not(_inAliveOrders(_toid))){ 374 | aliveOrders := List.push((_toid, Time.now()), aliveOrders); 375 | }; 376 | }; 377 | private func _removeTask(_toid: Toid, _ttid: Ttid) : (){ 378 | switch(orders.get(_toid)){ 379 | case(?(order)){ 380 | let tasks = List.filter(order.tasks, func (t:SagaTask): Bool{ t.ttid != _ttid }); 381 | let orderNew = { 382 | name = order.name; 383 | compStrategy = order.compStrategy; 384 | tasks = tasks; 385 | allowPushing = order.allowPushing; 386 | comps = order.comps; 387 | status = order.status; 388 | callbackStatus = order.callbackStatus; 389 | time = order.time; 390 | data = order.data; 391 | }; 392 | orders.put(_toid, orderNew); 393 | }; 394 | case(_){}; 395 | }; 396 | }; 397 | 398 | private func _removeTATaskByOid(_toid: Toid) : (){ 399 | ignore actuator().removeByOid(_toid); 400 | switch(orders.get(_toid)){ 401 | case(?(order)){ 402 | for (task in List.toArray(order.tasks).vals()){ 403 | taskCallback.delete(task.ttid); 404 | }; 405 | for (task in List.toArray(order.comps).vals()){ 406 | taskCallback.delete(task.tcid); 407 | }; 408 | }; 409 | case(_){}; 410 | }; 411 | }; 412 | 413 | private func _isOpening(_toid: Toid) : Bool{ 414 | switch(orders.get(_toid)){ 415 | case(?(order)){ return order.allowPushing == #Opening }; 416 | case(_){ return false; }; 417 | }; 418 | }; 419 | private func _allowPushing(_toid: Toid, _setting: {#Opening; #Closed; }) : (){ 420 | switch(orders.get(_toid)){ 421 | case(?(order)){ 422 | let orderNew = { 423 | name = order.name; 424 | compStrategy = order.compStrategy; 425 | tasks = order.tasks; 426 | allowPushing = _setting; 427 | comps = order.comps; 428 | status = order.status; 429 | callbackStatus = order.callbackStatus; 430 | time = order.time; 431 | data = order.data; 432 | }; 433 | orders.put(_toid, orderNew); 434 | }; 435 | case(_){}; 436 | }; 437 | }; 438 | private func _status(_toid: Toid) : ?OrderStatus{ 439 | switch(orders.get(_toid)){ 440 | case(?(order)){ 441 | return ?order.status; 442 | }; 443 | case(_){ return null; }; 444 | }; 445 | }; 446 | private func _statusEqual(_toid: Toid, _status: OrderStatus) : Bool{ 447 | switch(orders.get(_toid)){ 448 | case(?(order)){ 449 | return order.status == _status; 450 | }; 451 | case(_){ return false; }; 452 | }; 453 | }; 454 | // Set the status of TO to #Done. If an error occurs and cannot be caught, the status of TO is #Doing 455 | private func _orderComplete(_toid: Toid) : async* ?Status{ 456 | var callbackStatus : ?Status = null; 457 | switch(orders.get(_toid)){ 458 | case(?(order)){ 459 | for (task in List.toArray(order.tasks).vals()){ 460 | taskCallback.delete(task.ttid); 461 | }; 462 | for (comp in List.toArray(order.comps).vals()){ 463 | taskCallback.delete(comp.tcid); 464 | }; 465 | //try{ 466 | switch(orderCallback.get(_toid)){ 467 | case(?(_orderCallback)){ 468 | try{ 469 | countAsyncMessage += 2; 470 | await _orderCallback(order.name, _toid, order.status, order.data); 471 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 472 | orderCallback.delete(_toid); 473 | callbackStatus := ?#Done; 474 | }catch(e){ 475 | callbackStatus := ?#Error; 476 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 477 | }; 478 | }; 479 | case(_){ 480 | switch(defaultOrderCallback){ 481 | case(?(_orderCallback)){ 482 | try{ 483 | countAsyncMessage += 2; 484 | await _orderCallback(order.name, _toid, order.status, order.data); 485 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 486 | callbackStatus := ?#Done; 487 | }catch(e){ 488 | callbackStatus := ?#Error; 489 | countAsyncMessage -= Nat.min(2, countAsyncMessage); 490 | }; 491 | }; 492 | case(_){}; 493 | }; 494 | }; 495 | }; 496 | // } catch(e) { 497 | // callbackStatus := ?#Error; 498 | // }; 499 | }; 500 | case(_){}; 501 | }; 502 | return callbackStatus; 503 | }; 504 | private func _getTtids(_toid: Toid): [Ttid]{ 505 | var res : [Ttid] = []; 506 | switch(orders.get(_toid)){ 507 | case(?(order)){ 508 | for (task in List.toArray(order.tasks).vals()){ 509 | res := TA.arrayAppend(res, [task.ttid]); 510 | }; 511 | for (comp in List.toArray(order.comps).vals()){ 512 | res := TA.arrayAppend(res, [comp.tcid]); 513 | }; 514 | }; 515 | case(_){}; 516 | }; 517 | return res; 518 | }; 519 | private func _setStatus(_toid: Toid, _setting: OrderStatus) : (){ 520 | switch(orders.get(_toid)){ 521 | case(?(order)){ 522 | let orderNew = { 523 | name = order.name; 524 | compStrategy = order.compStrategy; 525 | tasks = order.tasks; 526 | allowPushing = order.allowPushing; 527 | comps = order.comps; 528 | status = _setting; 529 | callbackStatus = order.callbackStatus; 530 | time = order.time; 531 | data = order.data; 532 | }; 533 | orders.put(_toid, orderNew); 534 | }; 535 | case(_){}; 536 | }; 537 | }; 538 | private func _setCallbackStatus(_toid: Toid, _setting: ?Status) : (){ 539 | switch(orders.get(_toid)){ 540 | case(?(order)){ 541 | let orderNew = { 542 | name = order.name; 543 | compStrategy = order.compStrategy; 544 | tasks = order.tasks; 545 | allowPushing = order.allowPushing; 546 | comps = order.comps; 547 | status = order.status; 548 | callbackStatus = _setting; 549 | time = order.time; 550 | data = order.data; 551 | }; 552 | orders.put(_toid, orderNew); 553 | }; 554 | case(_){}; 555 | }; 556 | }; 557 | private func _isTasksDone(_toid: Toid) : Bool{ 558 | switch(orders.get(_toid)){ 559 | case(?(order)){ 560 | switch(List.pop(order.tasks)){ 561 | case((?(task), ts)){ 562 | return actuator().isCompleted(task.ttid); 563 | }; 564 | case(_){ return true; }; 565 | }; 566 | }; 567 | case(_){ return false; }; 568 | }; 569 | }; 570 | private func _isCompsDone(_toid: Toid) : Bool{ 571 | switch(orders.get(_toid)){ 572 | case(?(order)){ 573 | switch(List.pop(order.comps)){ 574 | case((?(task), ts)){ 575 | return actuator().isCompleted(task.tcid); 576 | }; 577 | case(_){ return false; }; 578 | }; 579 | }; 580 | case(_){ return false; }; 581 | }; 582 | }; 583 | private func _statusTest(_toid: Toid) : async* (){ 584 | switch(orders.get(_toid)){ 585 | case(?(order)){ 586 | if (order.status == #Doing and order.allowPushing == #Closed and _isTasksDone(_toid)){ 587 | _setStatus(_toid, #Done); 588 | var callbackStatus : ?Status = null; 589 | try{ 590 | callbackStatus := await* _orderComplete(_toid); 591 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != _toid }); 592 | }catch(e){ 593 | callbackStatus := ?#Error; 594 | _setStatus(_toid, #Blocking); 595 | }; 596 | _setCallbackStatus(_toid, callbackStatus); 597 | //await _orderComplete(_toid, #Done); 598 | } else if (order.status == #Compensating and order.allowPushing == #Closed and (_isCompsDone(_toid) or List.size(order.comps) == 0)){ 599 | _setStatus(_toid, #Recovered); 600 | var callbackStatus : ?Status = null; 601 | try{ 602 | callbackStatus := await* _orderComplete(_toid); 603 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != _toid }); 604 | }catch(e){ 605 | callbackStatus := ?#Error; 606 | _setStatus(_toid, #Blocking); 607 | }; 608 | _setCallbackStatus(_toid, callbackStatus); 609 | //await _orderComplete(_toid, #Recovered); 610 | _removeTATaskByOid(_toid); 611 | } else if (order.status == #Blocking and order.allowPushing == #Closed and _isTasksDone(_toid)){ 612 | // await _orderComplete(_toid, #Done); 613 | // _removeTATaskByOid(_toid); 614 | } else if (order.status == #Done or order.status == #Recovered){ 615 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != _toid }); 616 | }; 617 | }; 618 | case(_){ 619 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != _toid }); 620 | }; 621 | }; 622 | }; 623 | private func _orderLastCid(_toid: Toid) : ?Tcid{ 624 | switch(orders.get(_toid)){ 625 | case(?(order)){ 626 | switch(List.pop(order.comps)){ 627 | case((?(comp), ts)){ 628 | return ?comp.tcid; 629 | }; 630 | case(_){ return null; }; 631 | }; 632 | }; 633 | case(_){ return null; }; 634 | }; 635 | }; 636 | private func _pushComp(_toid: Toid, _ttid: Ttid, _comp: Compensation, _preTtid: ?[Ttid]) : Tcid{ 637 | if (not(_inOrders(_toid))){ return 0; }; 638 | let preTtid = Option.get(_orderLastCid(_toid), 0); 639 | let task: Task = { 640 | callee = _comp.callee; 641 | callType = _comp.callType; 642 | preTtid = Option.get(_preTtid, [preTtid]); 643 | toid = _comp.toid; 644 | forTtid = ?_ttid; 645 | attemptsMax = _comp.attemptsMax; 646 | recallInterval = _comp.recallInterval; 647 | cycles = _comp.cycles; 648 | data = _comp.data; 649 | time = Time.now(); 650 | }; 651 | let cid = actuator().push(task); 652 | let compTask: CompTask = { 653 | forTtid = _ttid; 654 | tcid = cid; 655 | comp = task; 656 | status = #Todo; //Todo 657 | }; 658 | switch(orders.get(_toid)){ 659 | case(?(order)){ 660 | let comps = List.push(compTask, order.comps); 661 | let orderNew = { 662 | name = order.name; 663 | compStrategy = order.compStrategy; 664 | tasks = order.tasks; 665 | allowPushing = order.allowPushing; 666 | comps = comps; 667 | status = order.status; 668 | callbackStatus = order.callbackStatus; 669 | time = order.time; 670 | data = order.data; 671 | }; 672 | orders.put(_toid, orderNew); 673 | }; 674 | case(_){}; 675 | }; 676 | return cid; 677 | }; 678 | private func _compensate(_toid: Toid, _errTask: Ttid) : (){ 679 | switch(orders.get(_toid)){ 680 | case(?(order)){ 681 | var tasks = order.tasks; 682 | var item = List.pop(tasks); 683 | while(Option.isSome(item.0)){ 684 | tasks := item.1; 685 | switch(item.0){ 686 | case(?(task)){ 687 | if (task.ttid < _errTask){ 688 | switch(task.comp){ 689 | case(?(comp)){ 690 | ignore _pushComp(_toid, task.ttid, comp, null); 691 | }; 692 | case(_){ // to block 693 | let comp: Compensation = { 694 | callee = task.task.callee; 695 | callType = #__block; 696 | preTtid = []; 697 | toid = ?_toid; 698 | forTtid = ?task.ttid; 699 | attemptsMax = 1; 700 | recallInterval = 0; // nanoseconds 701 | cycles = 0; 702 | data = null; 703 | time = Time.now(); 704 | }; 705 | ignore _pushComp(_toid, task.ttid, comp, null); 706 | }; 707 | }; 708 | }; 709 | }; 710 | case(_){}; 711 | }; 712 | item := List.pop(tasks); 713 | }; 714 | }; 715 | case(_){}; 716 | }; 717 | }; 718 | private func _setTaskStatus(_toid: Toid, _ttid: Ttid, _status: Status) : Bool{ 719 | var res : Bool = false; 720 | switch(orders.get(_toid)){ 721 | case(?(order)){ 722 | var tasks = order.tasks; 723 | var comps = order.comps; 724 | tasks := List.map(tasks, func (t:SagaTask): SagaTask{ 725 | if (t.ttid == _ttid){ 726 | res := true; 727 | return { 728 | ttid = t.ttid; 729 | task = t.task; 730 | comp = t.comp; 731 | status = _status; 732 | }; 733 | } else { return t; }; 734 | }); 735 | comps := List.map(comps, func (t:CompTask): CompTask{ 736 | if (t.tcid == _ttid){ 737 | res := true; 738 | return { 739 | forTtid = t.forTtid; 740 | tcid = t.tcid; 741 | comp = t.comp; 742 | status = _status; 743 | }; 744 | } else { return t; }; 745 | }); 746 | let orderNew : Order = { 747 | name = order.name; 748 | compStrategy = order.compStrategy; 749 | tasks = tasks; 750 | allowPushing = order.allowPushing; 751 | comps = comps; 752 | status = order.status; 753 | callbackStatus = order.callbackStatus; 754 | time = order.time; 755 | data = order.data; 756 | }; 757 | orders.put(_toid, orderNew); 758 | }; 759 | case(_){}; 760 | }; 761 | return res; 762 | }; 763 | private func __push(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _autoAddPreTtid: Bool) : Ttid{ 764 | assert(_inOrders(_toid) and _isOpening(_toid)); 765 | let task: TA.Task = _taskFromRequest(_toid, _task, _autoAddPreTtid); 766 | let tid = actuator().push(task); 767 | let comp = _compFromRequest(_toid, ?tid, _comp); 768 | let sagaTask: SagaTask = { 769 | ttid = tid; 770 | task = task; 771 | comp = comp; 772 | status = #Todo; //Todo 773 | }; 774 | _putTask(_toid, sagaTask); 775 | return tid; 776 | }; 777 | 778 | // The following methods are used for transaction order operations. 779 | 780 | /// Create a transaction order and return the transaction ID (toid) 781 | public func create(_name: Text, _compStrategy: CompStrategy, _data: ?Blob, _callback: ?OrderCallback) : Toid{ 782 | assert(this != Principal.fromText("aaaaa-aa")); 783 | let toid = index; 784 | index += 1; 785 | let order: Order = { 786 | name = _name; 787 | compStrategy = _compStrategy; 788 | tasks = List.nil>(); 789 | allowPushing = #Opening; 790 | comps = List.nil>(); 791 | progress = #Completed(0); 792 | status = #Todo; 793 | callbackStatus = null; 794 | time = Time.now(); 795 | data = _data; 796 | }; 797 | _pushOrder(toid, order); 798 | switch(_callback){ 799 | case(?(callback)){ orderCallback.put(toid, callback); }; 800 | case(_){}; 801 | }; 802 | return toid; 803 | }; 804 | 805 | /// Pushes a task to a specified transaction order. 806 | public func push(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?TaskCallback) : Ttid{ 807 | let ttid = __push(_toid, _task, _comp, true); 808 | switch(_callback){ 809 | case(?(callback)){ taskCallback.put(ttid, callback); }; 810 | case(_){}; 811 | }; 812 | return ttid; 813 | }; 814 | 815 | /// Sets the status of a task to #Done. requires that the transaction order the task is in is not complete and is in #Todo, 816 | /// #Doing, or #Blocking. 817 | public func taskDone(_toid: Toid, _ttid: Ttid, _toCallback: Bool) : async* ?Ttid{ 818 | if (_inAliveOrders(_toid) and not(actuator().isInPool(_ttid)) and not(actuator().isCompleted(_ttid))){ 819 | switch(orders.get(_toid)){ 820 | case(?(order)){ 821 | if ((order.status == #Todo or order.status == #Doing or order.status == #Blocking) and not(_isTasksDone(_toid))){ 822 | try{ 823 | let res = await* actuator().done(_ttid, _toCallback); 824 | return res; 825 | }catch(e){ 826 | return null; 827 | }; 828 | }; 829 | }; 830 | case(_){}; 831 | }; 832 | }; 833 | return null; 834 | }; 835 | 836 | /// task redo 837 | public func redo(_toid: Toid, _ttid: Ttid) : ?Ttid{ // Warning: proceed with caution! 838 | if (_inAliveOrders(_toid) and not(actuator().isInPool(_ttid)) and not(actuator().isCompleted(_ttid))){ 839 | switch(orders.get(_toid)){ 840 | case(?(order)){ 841 | if ((order.status == #Todo or order.status == #Doing or order.status == #Blocking) and not(_isTasksDone(_toid))){ 842 | //var taskStatus : Status = #Unknown; 843 | var task_ : ?Task = null; 844 | switch(List.find(order.tasks, func (t:SagaTask): Bool{ t.ttid == _ttid })){ 845 | case(?(sagaTask)){ task_ := ?sagaTask.task; }; 846 | case(_){}; 847 | }; 848 | switch(List.find(order.comps, func (t:CompTask): Bool{ t.tcid == _ttid })){ 849 | case(?(compTask)){ task_ := ?compTask.comp; }; 850 | case(_){}; 851 | }; 852 | switch(task_){ 853 | case(?(task)){ 854 | return ?(actuator().update(_ttid, task)); 855 | }; 856 | case(_){}; 857 | }; 858 | } else{}; 859 | }; 860 | case(_){}; 861 | }; 862 | }; 863 | return null; 864 | }; 865 | public func open(_toid: Toid) : (){ 866 | _allowPushing(_toid, #Opening); 867 | }; 868 | public func close(_toid: Toid) : (){ 869 | _allowPushing(_toid, #Closed); 870 | }; 871 | // @deprecated : It will be deprecated 872 | public func finish(_toid: Toid) : (){ 873 | close(_toid); 874 | }; 875 | // public func isEmpty(_toid: Toid) : Bool{ 876 | // switch(orders.get(_toid)){ 877 | // case(?(order)){ List.size(order.tasks) == 0 }; 878 | // case(_){ true }; 879 | // }; 880 | // }; 881 | public func run(_toid: Toid) : async ?OrderStatus{ 882 | switch(_status(_toid)){ 883 | case(?(#Todo)){ _setStatus(_toid, #Doing); }; 884 | case(_){}; 885 | }; 886 | let actuations = actuator().actuations(); 887 | if (actuations.actuationThreads < 5 or Time.now() > actuations.lastActuationTime + 60*1000000000){ // 60s 888 | try{ 889 | ignore await* actuator().run(); 890 | }catch(e){}; 891 | }; 892 | if (_toid > 0){ 893 | try{ 894 | await* _statusTest(_toid); 895 | }catch(e){}; 896 | }; 897 | return _status(_toid); 898 | }; 899 | public func runSync(_toid: Toid) : async ?OrderStatus{ 900 | switch(_status(_toid)){ 901 | case(?(#Todo)){ _setStatus(_toid, #Doing); }; 902 | case(_){}; 903 | }; 904 | let actuations = actuator().actuations(); 905 | if (actuations.actuationThreads > 10){ 906 | throw Error.reject("ICTC execution threads exceeded the limit."); 907 | }; 908 | ignore await* actuator().runSync(if (_toid > 0) { ?_getTtids(_toid) } else { null }); 909 | if (_toid > 0){ 910 | try{ await* _statusTest(_toid); }catch(e){}; 911 | }; 912 | return _status(_toid); 913 | }; 914 | 915 | // The following methods are used for queries. 916 | public func asyncMessageSize() : Nat{ 917 | return countAsyncMessage + actuator().actuations().countAsyncMessage; 918 | }; 919 | public func count() : Nat{ 920 | return index - 1; 921 | }; 922 | public func status(_toid: Toid) : ?OrderStatus{ 923 | return _status(_toid); 924 | }; 925 | public func isCompleted(_toid: Toid) : Bool{ 926 | return _statusEqual(_toid, #Done); 927 | }; 928 | public func isTaskCompleted(_ttid: Ttid) : Bool{ 929 | return actuator().isCompleted(_ttid); 930 | }; 931 | public func getOrder(_toid: Toid) : ?Order{ 932 | return orders.get(_toid); 933 | }; 934 | public func getOrders(_page: Nat, _size: Nat) : {data: [(Toid, Order)]; totalPage: Nat; total: Nat}{ 935 | return TA.getTM>(orders, index, firstIndex, _page, _size); 936 | }; 937 | public func getAliveOrders() : [(Toid, ?Order)]{ 938 | return Array.map<(Toid, Time.Time), (Toid, ?Order)>(List.toArray(aliveOrders), 939 | func (item:(Toid, Time.Time)):(Toid, ?Order) { 940 | return (item.0, orders.get(item.0)); 941 | }); 942 | }; 943 | public func getBlockingOrders() : [(Toid, Order)]{ 944 | return Array.mapFilter<(Toid, Time.Time), (Toid, Order)>(List.toArray(aliveOrders), 945 | func (item:(Toid, Time.Time)): ?(Toid, Order) { 946 | switch(orders.get(item.0)){ 947 | case(?order){ 948 | if (order.status == #Blocking){ 949 | return ?(item.0, order); 950 | }else{ 951 | return null; 952 | }; 953 | }; 954 | case(_){ return null }; 955 | }; 956 | }); 957 | }; 958 | public func getTaskEvents(_toid: Toid) : [TaskEvent]{ 959 | var events: [TaskEvent] = []; 960 | for (tid in Option.get(taskEvents.get(_toid), []).vals()){ 961 | let event_ = actuator().getTaskEvent(tid); 962 | switch(event_){ 963 | case(?(event)) { events := TA.arrayAppend(events, [event]); }; 964 | case(_){}; 965 | }; 966 | }; 967 | return events; 968 | }; 969 | // public func getTaskEvent(_ttid: Ttid) : ?TaskEvent{ 970 | // return actuator().getTaskEvent(_ttid); 971 | // }; 972 | // public func getAllEvents(_page: Nat, _size: Nat) : {data: [(Tid, TaskEvent)]; totalPage: Nat; total: Nat}{ 973 | // return actuator().getTaskEvents(_page, _size); 974 | // }; 975 | public func getActuator() : TA.TA{ 976 | return actuator(); 977 | }; 978 | 979 | 980 | // The following methods are used for clean up historical data. 981 | public func setCacheExpiration(_expiration: Int) : (){ 982 | autoClearTimeout := _expiration; 983 | }; 984 | public func clear(_expiration: ?Int, _delForced: Bool) : (){ 985 | _clear(_expiration, _delForced); 986 | actuator().clear(_expiration, _delForced); 987 | }; 988 | 989 | // The following methods are used for governance or manual compensation. 990 | /// update: Used to modify a task when blocking. 991 | public func update(_toid: Toid, _ttid: Ttid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?TaskCallback) : Ttid{ 992 | assert(_inOrders(_toid) and _isOpening(_toid) and not(isCompleted(_toid))); 993 | assert(not(actuator().isCompleted(_ttid))); 994 | let task: TA.Task = _taskFromRequest(_toid, _task, false); 995 | let tid = actuator().update(_ttid, task); 996 | let comp = _compFromRequest(_toid, ?tid, _comp); 997 | let sagaTask: SagaTask = { 998 | ttid = tid; 999 | task = task; 1000 | comp = comp; 1001 | status = #Todo; //Todo 1002 | }; 1003 | _updateTask(_toid, sagaTask); 1004 | taskCallback.delete(tid); 1005 | switch(_callback){ 1006 | case(?(callback)){ taskCallback.put(tid, callback); }; 1007 | case(_){}; 1008 | }; 1009 | return tid; 1010 | }; 1011 | /// remove: Used to undo an unexecuted task. 1012 | public func remove(_toid: Toid, _ttid: Ttid) : ?Ttid{ 1013 | assert(_inOrders(_toid) and _isOpening(_toid) and not(isCompleted(_toid))); 1014 | assert(not(actuator().isCompleted(_ttid))); 1015 | let tid_ = actuator().remove(_ttid); 1016 | _removeTask(_toid, _ttid); 1017 | taskCallback.delete(_ttid); 1018 | return tid_; 1019 | }; 1020 | /// append: Used to add a new task to an executing transaction order. 1021 | public func append(_toid: Toid, _task: PushTaskRequest, _comp: ?PushCompRequest, _callback: ?TaskCallback) : Ttid{ 1022 | assert(_inOrders(_toid) and _isOpening(_toid) and not(isCompleted(_toid))); 1023 | let ttid = __push(_toid, _task, _comp, false); 1024 | switch(_callback){ 1025 | case(?(callback)){ taskCallback.put(ttid, callback); }; 1026 | case(_){}; 1027 | }; 1028 | return ttid; 1029 | }; 1030 | public func appendComp(_toid: Toid, _forTtid: Ttid, _comp: PushCompRequest, _callback: ?TaskCallback) : Tcid{ 1031 | assert(_inOrders(_toid) and _isOpening(_toid) and not(isCompleted(_toid))); 1032 | let comp = _taskFromRequest(_toid, _comp, false); 1033 | let tcid = _pushComp(_toid, _forTtid, comp, ?comp.preTtid); 1034 | switch(_callback){ 1035 | case(?(callback)){ taskCallback.put(tcid, callback); }; 1036 | case(_){}; 1037 | }; 1038 | return tcid; 1039 | }; 1040 | /// complete: Used to change the status of a blocked order to completed. 1041 | public func complete(_toid: Toid, _status: OrderStatus) : async* Bool{ 1042 | assert(_status == #Done or _status == #Recovered); 1043 | if (_statusEqual(_toid, #Blocking) and not(_isOpening(_toid)) and (_isTasksDone(_toid) or _isCompsDone(_toid))){ 1044 | _setStatus(_toid, _status); 1045 | var callbackStatus : ?Status = null; 1046 | try{ 1047 | callbackStatus := await* _orderComplete(_toid); 1048 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != _toid }); 1049 | }catch(e){ 1050 | callbackStatus := ?#Error; 1051 | _setStatus(_toid, #Blocking); 1052 | }; 1053 | _setCallbackStatus(_toid, callbackStatus); 1054 | //await _orderComplete(_toid, _status); 1055 | _removeTATaskByOid(_toid); 1056 | return true; 1057 | }; 1058 | return false; 1059 | }; 1060 | public func doneEmpty(_toid: Toid) : Bool{ 1061 | if (_toid == 0){ 1062 | for ((toid, order) in orders.entries()){ 1063 | if (List.size(order.tasks) == 0 and List.size(order.comps) == 0){ 1064 | _setStatus(toid, #Done); 1065 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != toid }); 1066 | }; 1067 | }; 1068 | return true; 1069 | }else{ 1070 | switch(orders.get(_toid)){ 1071 | case(?(order)){ 1072 | if (List.size(order.tasks) == 0 and List.size(order.comps) == 0){ 1073 | _setStatus(_toid, #Done); 1074 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != _toid }); 1075 | return true; 1076 | }else{ 1077 | return false; 1078 | }; 1079 | }; 1080 | case(_){ return false; }; 1081 | }; 1082 | }; 1083 | }; 1084 | public func done(_toid: Toid, _status: OrderStatus, _toCallback: Bool) : async* Bool{ 1085 | assert(_status == #Done or _status == #Recovered); 1086 | if (_inAliveOrders(_toid) and not(_isOpening(_toid))){ 1087 | _setStatus(_toid, _status); 1088 | if(_toCallback){ 1089 | var callbackStatus : ?Status = null; 1090 | try{ 1091 | callbackStatus := await* _orderComplete(_toid); 1092 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != _toid }); 1093 | }catch(e){ 1094 | callbackStatus := ?#Error; 1095 | _setStatus(_toid, #Blocking); 1096 | }; 1097 | _setCallbackStatus(_toid, callbackStatus); 1098 | }else{ 1099 | aliveOrders := List.filter(aliveOrders, func (item:(Toid, Time.Time)): Bool{ item.0 != _toid }); 1100 | }; 1101 | //await _orderComplete(_toid, _status); 1102 | _removeTATaskByOid(_toid); 1103 | return true; 1104 | }; 1105 | return false; 1106 | }; 1107 | public func block(_toid: Toid) : ?Toid{ 1108 | if (_inAliveOrders(_toid)){ 1109 | switch(orders.get(_toid)){ 1110 | case(?(order)){ 1111 | if ((order.status == #Todo or order.status == #Doing or order.status == #Compensating) and 1112 | Time.now() > order.time + 30*60*1000000000){ 1113 | _setStatus(_toid, #Blocking); 1114 | return ?_toid; 1115 | }; 1116 | return null; 1117 | }; 1118 | case(_){ return null; }; 1119 | }; 1120 | }; 1121 | return null; 1122 | }; 1123 | 1124 | // The following methods are used for data backup and reset. 1125 | public func getData() : Data { 1126 | return { 1127 | autoClearTimeout = autoClearTimeout; 1128 | index = index; 1129 | firstIndex = firstIndex; 1130 | orders = Iter.toArray(orders.entries()); 1131 | aliveOrders = aliveOrders; 1132 | taskEvents = Iter.toArray(taskEvents.entries()); 1133 | actuator = actuator().getData(); 1134 | }; 1135 | }; 1136 | public func getDataBase() : Data { 1137 | let _orders = Iter.toArray(Iter.filter(orders.entries(), func (x: (Toid, Order)): Bool{ 1138 | x.1.time + 72*3600*1000000000 > Time.now() or List.some(aliveOrders, func (t: (Toid, Time.Time)): Bool{ x.0 == t.0 }) 1139 | })); 1140 | let _taskEvents = Iter.toArray(Iter.filter(taskEvents.entries(), func (x: (Toid, [Ttid])): Bool{ 1141 | Option.isSome(Array.find(_orders, func (t: (Toid, Order)): Bool{ x.0 == t.0 })) 1142 | })); 1143 | return { 1144 | autoClearTimeout = autoClearTimeout; 1145 | index = index; 1146 | firstIndex = firstIndex; 1147 | orders = _orders; 1148 | aliveOrders = aliveOrders; 1149 | taskEvents = _taskEvents; 1150 | actuator = actuator().getDataBase(); 1151 | }; 1152 | }; 1153 | public func setData(_data: Data) : (){ 1154 | autoClearTimeout := _data.autoClearTimeout; 1155 | index := _data.index; 1156 | firstIndex := _data.firstIndex; 1157 | orders := TrieMap.fromEntries(_data.orders.vals(), Nat.equal, TA.natHash); 1158 | aliveOrders := _data.aliveOrders; 1159 | taskEvents := TrieMap.fromEntries(_data.taskEvents.vals(), Nat.equal, TA.natHash); 1160 | actuator().setData(_data.actuator); 1161 | }; 1162 | 1163 | 1164 | }; 1165 | }; --------------------------------------------------------------------------------