DFlow supports App whitelist, whitelisted App canisters can receive notifications when a user opens a stream to the canister, there are 3 types of events: FlowCreation, FlowUpdate, FlowDeletion, FlowLiquidation.
type FlowType = variant { Constant };
type Flow = record {
id : text;
startTime : nat64;
deposit : nat;
sender : principal;
flowRate : nat;
flowType : FlowType;
settleFunds : nat;
receiver : principal;
settleTime : nat64;
};
The receiving canister must implement the following 4 functions to handle money flow event updates:
onFlowCreation: (args: Flow) -> ();
onFlowUpdate: (args: Flow) -> ();
onFlowDeletion: (args: Flow) -> ();
onFlowLiquidation: (args: Flow) -> ();
App canisters can also query flow information proactively using the following query interface:
getFlow: (id: Text) -> (Flow) query;
Below is an example App canister demonstrating how to integrate DFlow, it’s a simple App where users pay a subscription fee in order to use its service
.
import Result "mo:base/Result";
module {
public type FlowType = {
#Constant;
};
public type Flow = {
id : Text;
startTime : Nat64;
deposit : Nat;
sender : Principal;
flowRate : Nat;
flowType : FlowType;
settleFunds : Nat;
receiver : Principal;
settleTime : Nat64;
};
public type InitArgs = {
cap : ?Principal;
fee : Nat;
decimals : Nat8;
owner : ?Principal;
logo : Text;
name : Text;
underlyingToken : ?Principal;
symbol : Text;
};
public type Metadata = {
fee : Nat;
decimals : Nat8;
owner : Principal;
logo : Text;
name : Text;
totalSupply : Nat;
symbol : Text;
};
public type TxReceipt = { #Ok : Nat; #Err : TxError };
public type TxError = {
#InsufficientAllowance;
#InsufficientBalance;
#ErrorOperationStyle;
#Unauthorized;
#LedgerTrap;
#ErrorTo;
#Other : Text;
#BlockUsed;
#AmountTooSmall;
};
public type UserFlowsResponse = {
receiveFlows : [Flow];
sendFlows : [Flow];
};
public type UserInfoResponse = {
balance : Nat;
receiveFlows : [Flow];
liquidationDate : Nat64;
flowRate : Int;
sendFlows : [Flow];
};
public type DToken = actor {
addApp : shared(app: Principal) -> async TxReceipt;
addAuth : shared(user: Principal) -> async TxReceipt;
addOperator : shared(op: Principal) -> async TxReceipt;
allowance : (owner: Principal, spender: Principal) -> async Nat;
approve : shared(spender: Principal, value: Nat) -> async TxReceipt;
balanceOf : (user: Principal) -> async Nat;
burn : shared(user: Principal, value: Nat) -> async TxReceipt;
createFlow : shared(flowType: FlowType, sender: Principal, receiver: Principal, flowRate: Nat) -> async Result.Result<Text, TxError>;
deleteFlow : shared(id: Text) -> async TxReceipt;
getApps : () -> async [Principal];
getFlow : (id: Text) -> async ?Flow;
getLiquidationUser : () -> async [Principal];
getMetadata : () -> async [Metadata];
getUnderlyingToken : () -> async ?Principal;
getUserFlowRate : (user: Principal) -> async Int;
getUserFlows : (user: Principal) -> async UserFlowsResponse;
getUserInfo : (user: Principal) -> async UserInfoResponse;
getUserLiquidationDate : (user: Principal) -> async Nat64;
isOperator : (owner: Principal, spender: Principal) -> async Bool;
liquidateUser : shared(user: Principal) -> async TxReceipt;
mint : shared(user: Principal, value: Nat) -> async TxReceipt;
removeApp : shared(app: Principal) -> async TxReceipt;
removeAuth : shared(user: Principal) -> async TxReceipt;
removeOperator : shared(user: Principal) -> async TxReceipt;
setUnderlyingToken : shared(token: ?Principal) -> async ();
transfer : shared(to: Principal, value: Nat) -> async TxReceipt;
transferFrom : shared(from: Principal, to: Principal, value: Nat) -> async TxReceipt;
updateFlow : shared(id: Text, flowRate: Nat) -> async TxReceipt;
};
}
import TrieSet "mo:base/TrieSet";
import Nat "mo:base/Nat";
import Hash "mo:base/Hash";
import Error "mo:base/Error";
import Principal "mo:base/Principal";
import Option "mo:base/Option";
import Cycles "mo:base/ExperimentalCycles";
import Nat8 "mo:base/Nat8";
import Result "mo:base/Result";
import Prelude "mo:base/Prelude";
import DFlow "./dflow";
shared(msg) actor class ExampleApp(owner_: Principal) = this {
private stable var owner: Principal = owner_;
// the token used for subscription fee(test dUSDC)
private stable var feeTokenId: Principal = Principal.fromText("m65t2-zyaaa-aaaah-qc2ua-cai");
private stable var feeToken: DFlow.DToken = actor(Principal.toText(feeTokenId));
// assume subscription fee is 3600 unit dUSDC/hour, i.e. flowRate = 1 unit dUSDC/second
private stable var minFlowRate: Nat = 1;
// record subscribed users
private stable var users = TrieSet.empty<Principal>();
// the service provided by the App
public shared(msg) func service(): async Result.Result<Text, Text> {
// check if msg.caller in the subscription set
if(TrieSet.mem(users, msg.caller, Principal.hash(msg.caller), Principal.equal) == false) {
return #ok("hello world");
} else {
return #err("not a subscribed user!");
};
};
public query func getSubscribedUsers(): async TrieSet.Set<Principal> {
users
};
// service owner can withdraw fees
public shared(msg) func withdrawFees(amount: Nat): async DFlow.TxReceipt {
assert(msg.caller == owner);
await feeToken.transfer(msg.caller, amount)
};
public shared(msg) func onFlowCreation(flow: DFlow.Flow) : async () {
// check caller
assert(msg.caller == feeTokenId);
// check flow rate, if valid, put flow sender to subscribed set
if(flow.flowRate >= minFlowRate) {
users := TrieSet.put(users, flow.sender, Principal.hash(flow.sender), Principal.equal);
};
};
public shared(msg) func onFlowUpdate(flow: DFlow.Flow) : async () {
// check caller
assert(msg.caller == feeTokenId);
// check flow rate, if valid, put flow sender to subscribed set; if not, remove user
if(flow.flowRate >= minFlowRate) {
users := TrieSet.put(users, flow.sender, Principal.hash(flow.sender), Principal.equal);
} else {
users := TrieSet.delete(users, flow.sender, Principal.hash(flow.sender), Principal.equal);
};
};
public shared(msg) func onFlowDeletion(flow: DFlow.Flow) : async () {
// check caller
assert(msg.caller == feeTokenId);
users := TrieSet.delete(users, flow.sender, Principal.hash(flow.sender), Principal.equal);
};
public shared(msg) func onFlowLiquidation(flow: DFlow.Flow) : async () {
// check caller
assert(msg.caller == feeTokenId);
users := TrieSet.delete(users, flow.sender, Principal.hash(flow.sender), Principal.equal);
};
}