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.

1. Data structures

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;
};

2. Receive notifications

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) -> ();

3. Fetch flow information

App canisters can also query flow information proactively using the following query interface:

getFlow: (id: Text) -> (Flow) query;

4. Example App canister

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.

dtoken.mo:

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;
    };
}

app.mo:

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);
    };
}