簡介
今天我們繼續補充智慧合約的進階使用技巧,這次的主題是交易,合約內我們除了可以發起內聯action的呼叫,很多使用還需要直接呼叫其他的合約action或者以交易的形式呼叫自身的action。
發起交易/延時交易
在合約內可以非常方便的發起一個交易,無論是呼叫外部的合約action還是呼叫自身的,都很容易。
這裡可能你會有疑問,為何呼叫自身的action要通過發起交易的方式呢?一個最主要的原因是需要有交易記錄,如果直接作為內聯方法呼叫了,鏈上是看不到直觀的記錄的,而我們通過區塊鏈瀏覽器檢視交易時,是需要有交易記錄或者交易通知的,才能被查詢到。
構建交易
我們首先要引入#include <eosio/transaction.hpp>
,然後我們來看下面這個程式碼示例,這段程式碼來自系統合約eosio.system中delegate_bandwidth.cpp中changebw方法,這個方法在進行資源頻寬的抵押和贖回的時候都會呼叫。而下面的煮這段程式碼,是呼叫合約自身的refund退款方法,即在贖回時需要等待3天的贖回期,3天后退款交易會被執行,然後我們贖回的資源就能轉賬到我們的賬戶了。
//構建一個transaction eosio::transaction out; //配置action,包括了許可權、合約、action名稱和引數 out.actions.emplace_back( permission_level{from, active_permission}, get_self(), "refund"_n, from); //設定延時呼叫時間 out.delay_sec = refund_delay_sec; //取消已有延遲交易 eosio::cancel_deferred( from.value ); //傳送交易 out.send( from.value, from, true );
我們看到這裡使用的許可權是from的active許可權,而from就是發起贖回的賬號,因為這個是系統賬號,所以有特權,可以使用使用者許可權進行交易。
接著get_self()
引數就是指明瞭呼叫的合約就是當前自身合約。refund
就是呼叫的合約action,而後面這個from
則是呼叫refund傳遞的引數。
如果你希望交易立即執行,下面這句可以忽略out.delay_sec = refund_delay_sec;
這一句是設定了延時呼叫的時間,也就是這個交易不會立即執行,而是要等待refund_delay_sec
這麼多秒後才會執行,這個時間我們可以在eosio.system.hpp檔案中找到static constexpr uint32_t refund_delay_sec = 3 * seconds_per_day;
,也就是3天的時間。
eosio::cancel_deferred( from.value );
這一句的作用是取消一個延時交易,延時交易在發起的時候需要設定一個id,以便可以取消。為什麼需要取消呢?因為會導致重複執行。所以我們會用同一id來標識交易,比如from的值,這就是為什麼每個賬號只能有一個贖回交易,你發起多筆贖回時,贖回時間是是以你最後一次操作的時間來算的。
out.send( from.value, from, true );
傳送時,第一個引數就是sender_id,可用於取消交易;第二個引數是payer,也就是這個延時交易所佔用的RAM的支付者;最後一個引數是詢問是否替換調已存在的交易。
授權
發起交易需要注意一個問題,就是許可權的問題,呼叫任何的合約action我們都需要授權,即使是呼叫自身合約的action,也是需要授權的。
上面將構建交易的時候我們看到示例程式碼是用的是發起贖回的使用者賬戶的許可權,只因為這是系統合約,它有特權,所以可以直接使用使用者許可權,否則,如果普通合約想要這樣呼叫,就需要使用者授權給該合約,也就是使用者需要在自己的active許可權中增加合約授權,或者增加一個子許可權,授權給合約,然後連結合約特定方法。如圖:
圖中active有一個account,使用的是eosio.code
的許可權,這是一個內建的特殊許可權,用於合約呼叫的,所以這裡賦予了這個合約賬號可以使用該使用者active許可權的能力。另外active下還有一個新的admin許可權,將合約的setstatus方法連結到了這個許可權。這兩種方法都是用於授權呼叫合約的。
但是不同的是,active中的授權給合約賬號使用該使用者的active的許可權,而使用者的admin許可權只有呼叫合約setstatus的許可權,是兩個維度的操作,作用完全不同,具體關於許可權我們會在後面的文章中進行講解。
即使是合約呼叫合約自身的action,也需要給自己授權,如果合約中需要使用到active的許可權,就要將eosio.code
的許可權加到合約的active中。
可支付Action
我們有時會有這樣的需求,就是當使用者轉賬到合約的時候我們可以觸發一個合約方法,或者希望使用者呼叫合約方法的時候同時要支付一定的費用。
這就會用到合約中的交易通知的功能了,當使用者轉賬到合約時,我們的合約可以收到一個通知,然後進行其他的操作,這個功能是基於交易的通知機制,我們知道,當合約呼叫時,合約會收到一個呼叫通知,而轉賬的時候,我們使用require_recipient(from);
會讓from賬號也收到一個交易通知。所以即使Token的合約是別人的,我們利用這個機制,讓使用者轉賬到我們的合約賬號,那Token合約會收到通知,使用者和接收Token的合約也會接收到通知。
我們在合約中可以針對收到的通知進行處理,在新老CDT中,有不同的寫法,早期,都是通過重寫EOSIO_DISPATCH方法來實現。如下:
void route() { auto transfer_data = eosio::unpack_action_data<st_transfer>(); eosio_assert(transfer_data.quantity.is_valid(), "Invalid token transfer"); eosio_assert(transfer_data.quantity.amount > 0, "Quantity must be positive"); if (transfer_data.to != _self) { return; } require_auth(transfer_data.from); //TODO something } #undef EOSIO_DISPATCH #define EOSIO_DISPATCH(TYPE, MEMBERS) \ extern "C" \ { \ void apply(uint64_t receiver, uint64_t code, uint64_t action) \ { \ if (code == receiver) \ { \ switch (action) \ { \ EOSIO_DISPATCH_HELPER(TYPE, MEMBERS) \ } \ /* does not allow destructor of thiscontract to run: eosio_exit(0); */ \ } \ else if (action == "transfer"_n.value) \ { \ eosio::execute_action(eosio::name(receiver), "transfer"_n, &fisho::lucky::route); \ } \ } \ } EOSIO_DISPATCH(fisho::lucky, (setstatus))
其中,我們判斷了action是transfer的交易通知會去呼叫我們自身的route方法,在方法中我們還會進一步驗證這筆轉賬是否是從合法的Token合約而來,以防偽造交易。
現在的CDT版本已經簡化了這個功能,我們只需在方法上增加一個監聽的標註[[eosio::on_notify("*::transfer")]]
,其中*代表的是合約,transfer是action。比如:
[[eosio::on_notify("*::transfer")]] void transfer(name from, name to, asset quantity, string memo) { //TODO something }
注意:一定要主要驗證交易通知的來源,只驗證Token符號而忽略了合約的話,可能導致經濟損失,要確保收到的Token是來自於正確的合約,且符號位數是正確的。
總結
這次介紹的合約進階都是和交易相關的,是非常常用的功能,且也有很多坑,一定要非常小心,以免被黑客攻擊,造成經濟損失啊。