Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions src/deposits/Withdrawals.mo
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Result "mo:base/Result";
import Text "mo:base/Text";
import Time "mo:base/Time";
import TrieMap "mo:base/TrieMap";
import Nat32 "mo:base/Nat32";

import NNS "./NNS";
import Neurons "./Neurons";
Expand Down Expand Up @@ -291,6 +292,195 @@ module {
return withdrawal;
};

////// Support for subaccounts ///////////////

let nullBlob : Blob = "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00";

let ahash = (
func account_equal(a: Account, b: Account): Bool {
a.owner == b.owner and
(switch((a.subaccount, b.subaccount)) {
case (null, null) { true };
case(null, ?val){ not(nullBlob == val) };
case(?val, null){ not(nullBlob == val) };
case (?aSub, ?bSub) { Blob.equal(aSub, bSub)};
})
},
func account_hash(a: Account): Nat32 {
var accumulator = Principal.hash(a.owner);
switch(a.subaccount){
case(null){ accumulator +%= Blob.hash(nullBlob) };
case(?val){ accumulator +%= Blob.hash(val) };
};
return accumulator;
}
);

func accountToText(a: Account): Text {
Principal.toText(a.owner) # (
switch(a.subaccount){
case null { "" };
case ( ?sub ) {
if (sub == nullBlob) { "" } else{
Nat32.toText(Blob.hash(sub))
}
}
}
)
};

private var withdrawals_account = TrieMap.TrieMap<Text, Withdrawal_Account>(Text.equal, Text.hash);
private var withdrawalsByUser_account = TrieMap.TrieMap<Account, Buffer.Buffer<Text>>(ahash);

type Account = {owner: Principal; subaccount: ?Blob};

public type Withdrawal_Account = {
id: Text;
account: Account;
createdAt: Time.Time;
expectedAt: Time.Time;
readyAt: ?Time.Time;
disbursedAt: ?Time.Time;
total: Nat64;
pending: Nat64;
available: Nat64;
disbursed: Nat64;
};

public func createWithdrawal_account(account: Account, amount: Nat64, delay: Int): Withdrawal_Account{
let now = Time.now();
let id = nextForAccountWithdrawalId(account);
let withdrawal_account: Withdrawal_Account = {
id = id;
account = account;
createdAt = now;
expectedAt = now + (delay * second);
readyAt = null;
disbursedAt = null;
total = amount;
pending = amount;
available = 0;
disbursed = 0;
};
withdrawals_account.put(id, withdrawal_account);
pendingWithdrawals := Deque.pushBack(pendingWithdrawals, {id = id; createdAt = now});
withdrawalsByAccountAdd(withdrawal_account.account, id);
return {withdrawal_account with account}
};

private func nextForAccountWithdrawalId(account: Account): Text {
let count = Option.get<Buffer.Buffer<Text>>(
withdrawalsByUser_account.get(account),
Buffer.Buffer<Text>(0)
).size();
accountToText(account) # "-" # Nat.toText(count+1)
};

private func withdrawalsByAccountAdd(account: Account, id: Text) {
let buf = Option.get<Buffer.Buffer<Text>>(
withdrawalsByUser_account.get(account),
Buffer.Buffer<Text>(1)
);
buf.add(id);
withdrawalsByUser_account.put(account, buf);
};

public func withdrawalsFor_account(account: Account): [Withdrawal_Account] {
var sources = Buffer.Buffer<Withdrawal_Account>(0);
let ids = Option.get<Buffer.Buffer<Text>>(withdrawalsByUser_account.get(account), Buffer.Buffer<Text>(0));
for (id in ids.vals()) {
switch (withdrawals_account.get(id)) {
case (null) { P.unreachable(); };
case (?w) {
sources.add(w);
};
};
};
return sources.toArray();
};

public func completeWithdrawal_account(user: Account, amount: Nat64, to: NNS.AccountIdentifier): Result.Result<WithdrawalCompletion, WithdrawalsError> {
let now = Time.now();
let subaccount = switch (user.subaccount){
case null {nullBlob};
case (? subaccount ) {subaccount};
};

// Figure out which available withdrawals we're disbursing
var remaining : Nat64 = amount;
var b = Buffer.Buffer<(Withdrawal_Account, Nat64)>(1);
for (w in withdrawalsFor_account(user).vals()) {
if (remaining > 0 and w.available > 0) {
let applied = Nat64.min(w.available, remaining);
b.add((w, applied));
remaining -= applied;
};
};
// Check the user has enough available
if (remaining > 0) {
return #err(#InsufficientBalance);
};

// Update these withdrawal balances.
for ((w, applied) in b.vals()) {
let disbursedAt = if (w.disbursed + applied == w.total) {
?now
} else {
null
};
withdrawals_account.put(w.id, {
id = w.id;
account = w.account;
createdAt = w.createdAt;
expectedAt = w.expectedAt;
readyAt = w.readyAt;
disbursedAt = disbursedAt;
total = w.total;
pending = w.pending;
available = w.available - applied;
disbursed = w.disbursed + applied;
});
};

#ok({
transferArgs = {
memo : Nat64 = 0;
from_subaccount = ?Blob.toArray(subaccount);
to = Blob.toArray(to);
amount = { e8s = amount - icpFee };
fee = { e8s = icpFee };
created_at_time = ?{ timestamp_nanos = Nat64.fromNat(Int.abs(now)) };
};
failure = func() {
// To be called if the transfer has failed.

// Revert our changes, returning the withdrawal balance to
// the user.
for (({id}, applied) in b.vals()) {
let w = switch (withdrawals_account.get(id)) {
case (null) { P.unreachable() };
case (?w) { w };
};
withdrawals_account.put(w.id, {
id = w.id;
account = w.account;
createdAt = w.createdAt;
expectedAt = w.expectedAt;
readyAt = w.readyAt;
disbursedAt = null;
total = w.total;
pending = w.pending;
available = w.available + applied;
disbursed = w.disbursed - applied;
});
};
};
})
};


////////////////////////////////////////////////////////////////////

private func nextWithdrawalId(user: Principal): Text {
let count = Option.get<Buffer.Buffer<Text>>(
withdrawalsByUser.get(user),
Expand Down
141 changes: 135 additions & 6 deletions src/deposits/deposits.mo
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Result "mo:base/Result";
import Text "mo:base/Text";
import Time "mo:base/Time";
import TrieMap "mo:base/TrieMap";
import Nat8 "mo:base/Nat8";

import Daily "./Daily";
import ApplyInterest "./Daily/ApplyInterest";
Expand Down Expand Up @@ -426,6 +427,37 @@ shared(init_msg) actor class Deposits(args: {
NNS.accountIdToText(NNS.accountIdFromPrincipal(Principal.fromActor(this), NNS.principalToSubaccount(msg.caller)));
};

//////////////////////////////// for subaccount functions /////////////////////////////

private func accountToSubaccount(account: Account.Account): Blob {
let ownerToSubaccount = NNS.principalToSubaccount(account.owner);
switch(account.subaccount) {
case (null) { return ownerToSubaccount};
case (?subaccount) {
let a1 = Blob.toArray(ownerToSubaccount);
let a2 = Blob.toArray(subaccount);
let combined = Array.tabulate<Nat8>(
32,
func(i) {
Nat8.fromNat((Nat8.toNat(a1[i]) + Nat8.toNat(a2[i])) % 256)
}
);
Blob.fromArray(combined)
};
}
};

public shared query(msg) func getDepositAddressForSubaccount(subaccount: ?Blob): async Text {
let userSubaccount = accountToSubaccount({ owner = msg.caller; subaccount});
NNS.accountIdToText(NNS.accountIdFromPrincipal(Principal.fromActor(this), userSubaccount));
};

public shared ({ caller }) func depositIcpForSubaccount(subaccount: Blob): async DepositReceipt{
await doDepositIcpFor(caller, ?subaccount);
};

////////////////////////////////////////////////////////////////////////////////////

// Same as getDepositAddress, but allows the canister owner to find it for
// a specific user.
public shared query(msg) func getDepositAddressFor(user: Principal): async Text {
Expand Down Expand Up @@ -465,19 +497,20 @@ shared(init_msg) actor class Deposits(args: {
// After the user transfers their ICP to their depositAddress, process the
// deposit, be minting the tokens.
public shared(msg) func depositIcp(): async DepositReceipt {
await doDepositIcpFor(msg.caller);
await doDepositIcpFor(msg.caller, null);
};

// After the user transfers their ICP to their depositAddress, process the
// deposit, be minting the tokens.
public shared(msg) func depositIcpFor(user: Principal): async DepositReceipt {
owners.require(msg.caller);
await doDepositIcpFor(user)
await doDepositIcpFor(user, null)
};

private func doDepositIcpFor(user: Principal): async DepositReceipt {
private func doDepositIcpFor(user: Principal, _subaccount: ?Blob): async DepositReceipt {
// Calculate target subaccount
let subaccount = NNS.principalToSubaccount(user);
// let subaccount = NNS.principalToSubaccount(user);
let subaccount = accountToSubaccount({ owner = user; subaccount = _subaccount });
let sourceAccount = NNS.accountIdFromPrincipal(Principal.fromActor(this), subaccount);

// Check ledger for value
Expand Down Expand Up @@ -534,7 +567,7 @@ shared(init_msg) actor class Deposits(args: {
// Mint the new tokens
Debug.print("[Referrals.convert] user: " # debug_show(user));
referralTracker.convert(user, ?now);
let userAccount = {owner=user; subaccount=null};
let userAccount = {owner=user; subaccount= ?subaccount};
ignore queueMint(userAccount, toMintE8s);
ignore flushMint(userAccount);

Expand Down Expand Up @@ -814,7 +847,14 @@ shared(init_msg) actor class Deposits(args: {
return #err(#InsufficientLiquidity);
};

return #ok(withdrawals.createWithdrawal(user.owner, toUnlockE8s, delay));
// return #ok(withdrawals.createWithdrawal(user.owner, toUnlockE8s, delay));
return #ok(
if(user.subaccount == null){
withdrawals.createWithdrawal(user.owner, toUnlockE8s, delay)
} else {
{withdrawals.createWithdrawal_account(user, toUnlockE8s, delay) with user = msg.caller}
}
)
};

// Complete withdrawal(s), transferring the ready amount to the
Expand Down Expand Up @@ -896,6 +936,95 @@ shared(init_msg) actor class Deposits(args: {
result
};

///////////////// Complete Withdrawal for account ///////////////////////////

type Account = {owner: Principal; subaccount: ?Blob};

public shared(msg) func completeWithdrawal_account(account: Account, amount: Nat64, to: Text): async Withdrawals.PayoutResult {
let user = account.owner;
let subaccount = switch (account.subaccount) {
case null {NNS.defaultSubaccount()};
case (?subaccount) {subaccount};
};

if (msg.caller != user) {
owners.require(msg.caller);
};

// See if we got a valid address to send to.
//
// Try to parse text as an address or a principal. If a principal, return
// the default subaccount address for that principal.
let toAddress = switch (NNS.accountIdFromText(to)) {
case (#err(_)) {
// Try to parse as a principal
try {
NNS.accountIdFromPrincipal(Principal.fromText(to), subaccount)
} catch (error) {
return #err(#InvalidAddress);
};
};
case (#ok(toAddress)) {
if (NNS.validateAccountIdentifier(toAddress)) {
toAddress
} else {
return #err(#InvalidAddress);
}
};
};

// Check we think we have enough cash available to fulfill this.
// We can't use _availableBalance here, because it subtracts out the
// withdrawals.reservedICP, which is what we actually want to use for
// this fulfillment.
if (Nat64.min(cachedLedgerBalanceE8s, withdrawals.reservedIcp()) < amount + pendingTransfers.reservedIcp()) {
return #err(#InsufficientLiquidity);
};


// Mark withdrawals as complete, and the balances as "disbursed"
let {transferArgs; failure} = switch (withdrawals.completeWithdrawal(user, amount, toAddress)) {
case (#err(err)) { return #err(err); };
case (#ok(a)) { a };
};

// Mark the funds as unavailable while the transfer is pending.
let transferId = pendingTransfers.add(amount);

// Attempt the transfer, reverting if it fails.
let result = try {
let transfer = await ledger.transfer(transferArgs);
switch (transfer) {
case (#Ok(block)) {
pendingTransfers.success(transferId);
#ok(block)
};
case (#Err(#InsufficientFunds{})) {
// Not enough ICP in the contract
pendingTransfers.failure(transferId);
failure();
#err(#InsufficientLiquidity)
};
case (#Err(err)) {
pendingTransfers.failure(transferId);
failure();
#err(#TransferError(err))
};
}
} catch (error) {
pendingTransfers.failure(transferId);
failure();
#err(#Other(Error.message(error)))
};

// Queue a balance refresh.
ignore refreshAvailableBalance();
result
};


/////////////////////////////////////////////////////////////////////////////

// List all withdrawals for a user.
// public shared(msg) func listWithdrawals(user: Principal) : async [Withdrawals.Withdrawal] {
public func listWithdrawals(user: Principal) : async [Withdrawals.Withdrawal] {
Expand Down