diff --git a/.gitignore b/.gitignore index a4aeb2c..d4c73a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env # Node node_modules @@ -11,6 +12,10 @@ node_modules # DFX .dfx src/declarations +canister_ids.json # Azle .azle + +.mops + diff --git a/.vscode/settings.json b/.vscode/settings.json index 65a1965..0574f25 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[motoko]": { + "editor.defaultFormatter": "dfinity-foundation.vscode-motoko" + } } diff --git a/Anotaciones.md b/Anotaciones.md new file mode 100644 index 0000000..2dca025 --- /dev/null +++ b/Anotaciones.md @@ -0,0 +1,63 @@ +### Verificacion de usuarios: + +Se deja implementada una funcion para evaluar el estado de verificacion de un usuario **`userIsVerificated()`** mediante la cual, en un contexto de produccion, se permitirá la publicacion de espacios de alojamiento solo cuando el usuario publicante esté verificado mediante algun tipo de procedimiento KYC. +En un contexto de MVP todos los usuarios serán inicializados por defecto como verificados. + +--- + +1: Solicitud de reserva. + A: Se evalua si el la fecha de la reserva es mayor al tiempo actual mas el tiempo minimo fijado por el host, o sea si el host dice que se puede reservar con 24 horas de anticipación y el usuario quiere reserva para dentro de 10 horas, se devuelve un error. + B: Si todo va bien, el backend devuelve los datos de la solicitud mas un codigo de pago. + C: Cuando se arma la transacción en el front, el codigo de pago se pone en el campo Memo y se hace la transacción. +2: Tiempo de bloqueo configurable (40 minutos por ejemplo): + A: Durante este plaso de tiempo se marca como no disponible o pre reservado todo el rango de tiempo correspondiente a la reserva. + B: El usuario tiene tiempo en este plaso (40 minutos segun ejemplo), de proceder con el pago de confirmación. + C: Si el plaso finaliza sin que se haya concretado la confirmacion, se vuuelve a marcar como disponible. +2: Confirmación de reserva + A: Si el usuario realiza la transaccián, la cual devuelve un transaction hash, se llama a otra funcion de backend enviando el id de la reserva mas el transaction hash. + B: Mediante una consulta al Ledger correspondiente a la moneda de pago, desde el bakend se envia el transaction hash, confirmando que los datos de retorno sean los correspondientes a la transaccion solicitada (Campo memo). + C: Luego de la confirmacion y verificación se marca como ocupado en el calendario el rango de tiempo de alojamineto + + +--- +#### Modificación del flujo de confirmaciones de reservas. (Confirmación del lado del Host) +##### Ventajas y desventajas + +### Ventajas: +1. El Host puede elegir, para un mismo periodo de alojamiento, el huesped que mejor se acomode a sus conveniencias de entre todos los que hayan requerido ese periodo de alojamiento. + +### Desventajas: +#### Desventajas Para la plataforma: +1. Por cada solicitud de reserva, la platafoma tiene que enviar una notificación al dueño de Host y esperar respuesta +2. Durante el tiempo de espera, para ese mismo periodo de hospedaje solicitado se pueden acumular mas solicitudes, las cuales tienen que ser notificadas también. +3. Que el usuario salga de la plataforma sin haber concretado un pago es motivo suficiente para que no vuelva. +3. Cuando el dueño del Host confirma, la plataforma tiene que notificar tanto al potencial huesped como a los rechazados +4. Al tiempo de demora de la confirmación hay que sumarle el tiempo de demora de confirmacion de la confirmacion por parte del huesped. +4. Es decir, el potencial huesped tiene que reponder de alguna manera a la confirmacion. ¿Mediante un pago? +5. Es incierto el momento en el que se establece definitivamente en el calendario un periodo de alojamiento como ocupado +#### Desventajas Para el dueño: +1. La ventaja de poder elegir es equivalente a la desventaja de tener que elegir en cualquier momento del dia y rápido. Filosoficamente: No es una elección tener que elegir ya +2. Si el tiempo de demora de la confirmación supera los 20 o 30 minutos, es muy probable que en ese momento el potencial huesped ya haya conseguido hospedaje en otro lugar. +3. Posiblemente los rechazados no vuelven nunca más e incluso pidan explicaciones, que de no ser satisfechas consistentemente generen problemas legales. +4. La confirmación de una reserva para un periodo largo de alojamiento y que luego no se materializa en un hospedaje (porque el usuario ya encontro otro lugar u otros motivos) puede tener como consecuencia, el rechazo de multiples solicitudes de alojamiento para ese mismo periodo y que no necesariamente hayan tenido solapamientos entre si. +##### Ejemplo: +##### Solicitud confirmada: ++ dias [20... 30] +##### Solicitudes rechazadas: ++ dias [20... 24], ++ dia 25, ++ dias [27... 28] ++ dia 29, ++ dia 30 + +#### Desventajas Para el Usuario: +1. Que el usuario salga de la plataforma con las manos vacias pudiendo salir con un problema solucionado es algo evitable. + +#### Notas mentales. Volumen 2 +##### Comisiones por alojmiento +Para las comisiones por alojamiento se puede establecer un porcentage del monto final, el cuál será deducido del monto recibido por el Housing en funcion del precio publicado. +Para disminuir la friccion del usuario final, el cobro de la comisión puede hacerse directamente mediante un transfer_from luego de la recepcion de fondos en la wallet del Housing. +Para poder proceder con ese transfer_from es necesario que la wallet del housing, haya firmado un approve y para eso puede ser conveniente hacerlo durante la creacion del Housing. +En este proceso ya quedaría establecida la wallet del housing y además se habria adquirido la firma del approve en favor de la plataforma. +Actualmente la wallet receptora de fondos de un housing se calcula a partir del Principal ID del owner de ese Housing, de manera tal que es unicamente ese principal quien puede moverlos, lo cual está bien pero para ello hay que implementar una funcion con la que el usuario pueda desde la plataforma hacer transferencias de esos tokens hacia una wallet o hacia algun exchange. +La opcion de conectar una wallet durante la creacion del housing elimina la necesidad de desarrollar un mecanismo de withdraw extra ya que el dueño del Housing puede visualizar su balance directamente en su plug wallet o ser notificado automaticamente cada vez que recibe el pago por algun alojamiento. diff --git a/backend/constants.mo b/backend/constants.mo new file mode 100644 index 0000000..4a0cbc8 --- /dev/null +++ b/backend/constants.mo @@ -0,0 +1,25 @@ +module { + + public let PayRequest = "Please have 60 minutes to complete the payment process and secure your reservation. After this period, if the transaction has not been completed, the reservation request will be cancelled. Please do not proceed with payment after the deadline has expired."; + public let NotAvalableAllDays = "At least one of the requested days is not available"; + public let NotHousing = "There is no housing associated with the ID provided"; + public let NotReservation = "There is no reservation associated with the ID provided"; + public let PaginationOutOfRange = "Pagination index out of range"; + public let CallerNotHousingOwner = "The caller is not the owner of the hosting"; + public let UnauthorizedCaller = "Unauthorized user"; + public let NotVerifiedUser = "The user is not verified"; + public let NotUser = "Unregistered user"; + public let NotAdmin = "The caller is not admin"; + public let Anonymous = "Anonymous caller is not allowed"; + public let NotHostUser = "User is not Host User"; + public let CallerIsNotrequester = "The caller does not match the reservation requester"; + public let InactiveHousing = "Housing is temporarily disabled"; + public let ZeroIsNotAllowed = "Is not greater than zero"; + public let IsNotpublishable = "Not publishable due to missing data"; + public let HousingTypeExist = "The housing type already exists"; + public let HousingTypeNoExist = "The type of housing does not exist"; + public let CallerIsNotRequester = "The caller is not the requester of the reservation id number "; + public let TransactionNotVerified = "The transaction was not verified successfully"; + public let ErrorSetHoursCheckInCheckOut = "Check-in for one accommodation must be at least one hour later than check-out for the previous accommodation"; + public let ErrorCheckInCheckOutDays = "The CheckOut day must be at least one day after the CheckIn day." +}; diff --git a/backend/indexer_icp_token.mo b/backend/indexer_icp_token.mo new file mode 100644 index 0000000..febf8cf --- /dev/null +++ b/backend/indexer_icp_token.mo @@ -0,0 +1,80 @@ +// This is a generated Motoko binding. +// Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. + +module { + public type Account = { owner : Principal; subaccount : ?Blob }; + public type GetAccountIdentifierTransactionsArgs = { + max_results : Nat64; + start : ?Nat64; + account_identifier : Text; + }; + public type GetAccountIdentifierTransactionsError = { message : Text }; + public type GetAccountIdentifierTransactionsResponse = { + balance : Nat64; + transactions : [TransactionWithId]; + oldest_tx_id : ?Nat64; + }; + public type GetAccountIdentifierTransactionsResult = { + #Ok : GetAccountIdentifierTransactionsResponse; + #Err : GetAccountIdentifierTransactionsError; + }; + public type GetAccountTransactionsArgs = { + max_results : Nat; + start : ?Nat; + account : Account; + }; + public type GetBlocksRequest = { start : Nat; length : Nat }; + public type GetBlocksResponse = { blocks : [Blob]; chain_length : Nat64 }; + public type HttpRequest = { + url : Text; + method : Text; + body : Blob; + headers : [(Text, Text)]; + }; + public type HttpResponse = { + body : Blob; + headers : [(Text, Text)]; + status_code : Nat16; + }; + public type InitArg = { ledger_id : Principal }; + public type Operation = { + #Approve : { + fee : Tokens; + from : Text; + allowance : Tokens; + expected_allowance : ?Tokens; + expires_at : ?TimeStamp; + spender : Text; + }; + #Burn : { from : Text; amount : Tokens; spender : ?Text }; + #Mint : { to : Text; amount : Tokens }; + #Transfer : { + to : Text; + fee : Tokens; + from : Text; + amount : Tokens; + spender : ?Text; + }; + }; + public type Status = { num_blocks_synced : Nat64 }; + public type TimeStamp = { timestamp_nanos : Nat64 }; + public type Tokens = { e8s : Nat64 }; + public type Transaction = { + memo : Nat64; + icrc1_memo : ?Blob; + operation : Operation; + timestamp : ?TimeStamp; + created_at_time : ?TimeStamp; + }; + public type TransactionWithId = { id : Nat64; transaction : Transaction }; + public type Self = InitArg -> async actor { + get_account_identifier_balance : shared query Text -> async Nat64; + get_account_identifier_transactions : shared query GetAccountIdentifierTransactionsArgs -> async GetAccountIdentifierTransactionsResult; + get_account_transactions : shared query GetAccountTransactionsArgs -> async GetAccountIdentifierTransactionsResult; + get_blocks : shared query GetBlocksRequest -> async GetBlocksResponse; + http_request : shared query HttpRequest -> async HttpResponse; + icrc1_balance_of : shared query Account -> async Nat64; + ledger_id : shared query () -> async Principal; + status : shared query () -> async Status; + } +} \ No newline at end of file diff --git a/backend/main.mo b/backend/main.mo new file mode 100644 index 0000000..0460aa6 --- /dev/null +++ b/backend/main.mo @@ -0,0 +1,1398 @@ +import Prim "mo:⛔"; +import Array "mo:base/Array"; +import Blob "mo:base/Blob"; +import Buffer "mo:base/Buffer"; +import Int "mo:base/Int"; +import Iter "mo:base/Iter"; +import List "mo:base/List"; +import Nat "mo:base/Nat"; +import Nat8 "mo:base/Nat8"; +import Nat64 "mo:base/Nat64"; +import Principal "mo:base/Principal"; +import Text "mo:base/Text"; +import { now } "mo:base/Time"; +import { print } "mo:base/Debug"; + +import Map "mo:map/Map"; +import Set "mo:map/Set"; +import { phash; nhash; n32hash; thash } "mo:map/Map"; +import Indexer_icp "./indexer_icp_token"; +import AccountIdentifier "mo:account-identifier"; +import IC "ic:aaaaa-aa"; + +import Minter "../tour/minter-canister"; + +import Types "types"; +import msg "constants"; + +shared ({ caller = DEPLOYER }) actor class Triourism () = this { + + type User = Types.User; + type HostUser =Types.HostUser; + type UserData = Types.UserData; + type SignUpResult = Types.SignUpResult; + type Calendary = Types.Calendary; + type Reservation = Types.Reservation; + type HousingId = Nat; + type ReviewId = Nat; + type Review = Types.Review; + type Housing = Types.Housing; + type HousingTypeInit = Types.HousingTypeInit; + type HousingType = Types.HousingType; + type HousingResponse = Types.HousingResponse; + type HousingPreview = Types.HousingPreview; + type HousingCreateData = Types.HousingCreateData; + type TransactionParams = Types.TransactionParams; + type DataTransaction = Types.DataTransaction; + type TransactionResponse = Types.TransactionResponse; + + type UpdateResult = Types.UpdateResult; + type ResultHousingPaginate = {#Ok: {array: [HousingPreview]; hasNext: Bool}; #Err: Text}; + type RewardRatio = { + #ICP_DIVIDES_TOUR: Nat; // Reward = Ammount * Relación. Ejemplo para ICPvTourRewardRatio = #ICP_DIVIDES_TOUR(2): txAmount = 100 ICP -> recompenza = 200 Tour + #TOUR_DIVIDES_ICP: Nat; // Reward = Ammount / Relación. Ejemplo para ICPvTourRewardRatio = #TOUR_DIVIDES_ICP(2): txAmount = 100 ICP -> recompenza = 50 Tour + }; + + + // stable let DEPLOYER = caller; + let NULL_ADDRESS = "aaaaa-aa"; + + stable var TourMinterCanister: Minter.Minter = actor(NULL_ADDRESS); + stable var TourLedgerCanisterID = NULL_ADDRESS; + + // /////////////// WARNING modificar estas variables en produccion a los valores reales //// + let nanoSecPerDay = 30 * 1_000_000_000; // Test Transcurso acelerado de los dias + // let nanoSecPerDay = 86400 * 1_000_000_000; // Valor real de nanosegundos en un dia + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////// Global Configuration Parameters ////////////////////////////////////// + + stable var CancellationFeeCompensateBuyer: Nat64 = 5; // Percentage added to the buyer's refund + stable var ReservationFee: Nat64 = 10; // Percentage of the total reservation price + stable var TimeToPay = 15 * 1_000_000_000; // Tiempo en nanosegundos para confirmar la reserva mediante pago + // stable var TimeToPay = 30 * 60 * 1_000_000_000; // Tiempo sugerido 30 minutos + stable var MinDaysBeforeCheckinForCancellation = 4; // Minimo de dias antes del checkin para cancelar una reserva pagando CancellationFeeCompensateBuyer + + stable var ICPvTourRewardRatio: RewardRatio = #TOUR_DIVIDES_ICP(2); // Cambiar por un Nat + stable var RewardRatio: Nat64 = 1000; // eg. RewardRatio = 1000; -> 1000 USD = 1 Tour + + //////////////////////////////// Core Data Structures /////////////////////// + + stable let admins = Set.new(); + ignore Set.put(admins, phash, DEPLOYER); + + stable let users = Map.new(); + stable let hostUsers = Map.new(); + + stable let housings = Map.new(); + stable let review = Map.new(); + + stable let reservationsPendingConfirmation = Map.new(); + stable let reservationsHistory = Map.new(); + + stable var lastHousingId = 0; + stable var lastReviewId = 0; + stable var lastReservationId = 0; + + stable let referralCodes = Map.new(); + + // Prueba Amenidades dinamicas + stable var amenities: [Text] = Types.amenitiesArray; + + public shared ({ caller }) func addAmenities(a: Text): async (){ + assert(isAdmin(caller)); + // TODO Normalizar cadenas de texto y evitar duplicados + let setAmenities = Set.fromIter(amenities.vals(), thash); + ignore Set.put(setAmenities, thash, a); + amenities := Set.toArray(setAmenities); + // TODO Modificar encodedAmenities en cada housing + }; + + ///////////////////////////////////// Login Update functions //////////////////////////////////////// + + public shared ({ caller }) func signUpAsUser(data: Types.SignUpData) : async SignUpResult { + if(Principal.isAnonymous(caller)) { return #Err(msg.Anonymous) }; + if (Map.has(users,phash, caller )){ + return #Err("The caller is linked to an existing User Host") + }; + let user = Map.get(users, phash, caller); + switch user { + case (?User) { #Err("User already exists") }; + case null { + + let referralProcess = putRefered(data.referralBy, caller, #User(#Level1)); + if (referralProcess){ + let newUser: User = { + data with + verified = true; + reviewsIssued = List.nil(); //Reservation IDs in reservationsPendingConfirmation + reservations = List.nil(); //Reservation IDs in reservationsHistory + score = 0; + }; + ignore Map.put(users, phash, caller, newUser); + #Ok( newUser ); + } else { + #Err("Invalid referral code") + } + }; + }; + }; + + public shared ({ caller }) func signUpAsHost(data: Types.SignUpData) : async SignUpResult { + if(Principal.isAnonymous(caller)) { return #Err(msg.Anonymous) }; + if (Map.has(users,phash, caller )){ + return #Err("The caller is linked to an existing User") + }; + let hostUser = Map.get(hostUsers, phash, caller); + switch hostUser { + case (?User) { #Err("Host User already exists") }; + case null { + let referralProcess = putRefered(data.referralBy, caller, #User(#Level1)); + if (referralProcess){ + let newHostUser: HostUser = { + data with + verified = true; + score = 0; + housingIds = List.nil(); + housingTypes = Map.new(); + }; + ignore Map.put(hostUsers, phash, caller, newHostUser); + #Ok(newHostUser); + } else { + #Err("Invalid referral code") + } + }; + }; + }; + + public shared query ({ caller }) func loginAsUser(): async {#Ok: UserData; #Err} { + let user = Map.get(users, phash, caller); + switch user { + case null { #Err() }; + case ( ?u ) { #Ok(u)} + }; + }; + + public shared query ({ caller }) func loginAsHost(): async {#Ok: UserData; #Err} { + let hostUser = Map.get(hostUsers, phash, caller); + switch hostUser { + case null { #Err() }; + case ( ?u ) { #Ok(u)} + }; + }; + + public shared ({ caller }) func getMyReferralCode(): async Nat32 { + let code = (Principal.hash(caller)); + let referralBook = Map.get(referralCodes, n32hash, code); + switch referralBook { + case null { + let book: Types.ReferralBook = { + owner = caller; + refereds: [Types.Refered] = []; + }; + ignore Map.put(referralCodes, n32hash, code, book); + }; + case _ {} + }; + code + }; + + public shared ({ caller }) func getMyReferralBook(): async ?Types.ReferralBook{ + Map.get(referralCodes, n32hash, Principal.hash(caller)); + }; + + func putRefered(code: ?Nat32, user: Principal, kind: Types.ReferalKind): Bool { + switch (code) { + case null { true }; + case ( ?code ) { + let referralBook = Map.get(referralCodes, n32hash, code); + switch referralBook { + case null { false }; + case (?referralBook) { + let refered = { date = now(); user; kind }; + let refereds = Prim.Array_tabulate( + referralBook.refereds.size() +1, + func x = if( x == 0 ) { refered } else { referralBook.refereds[x -1]} + ); + ignore Map.put( + referralCodes, + n32hash, + code, + {referralBook with refereds } + ); + true + } + }; + } + } + }; + + + //////////////////////////////// CRUD Data User /////////////////////////////////// + + public shared ({ caller }) func editProfile(data: Types.SignUpData): async {#Ok; #Err}{ + let user = Map.get(users, phash, caller); + switch user { + case null { #Err }; + case (?user){ + ignore Map.put(users, phash, caller, { user with data}); + #Ok + }; + }; + }; + + ///////////////////////////// Private functions /////////////////////////////////// + func isAdmin(p: Principal): Bool { Set.has(admins, phash, p) }; + + func addressEqual(a: Types.Location, b: Types.Location ) : Bool { + a.country == b.country and + a.city == b.city and + a.neighborhood == b.neighborhood and + a.zipCode == b.zipCode and + a.externalNumber == b.externalNumber and + a.internalNumber == b.internalNumber + }; + + let NULL_LOCATION: Types.Location = { + country = ""; + city = ""; + neighborhood = ""; + zipCode = 0; street = ""; + externalNumber = 0; + internalNumber = 0; + geolocation = null; + }; + + let defaultHousinValues = { + active: Bool = false; + rules: [Types.Rule] = []; + price: ?Types.Price = null; + checkIn: Nat = 15; + checkOut: Nat = 12; + address: Types.Location = NULL_LOCATION; + properties: ?HousingType = null; + housingType: ?Text = null; + amenities = null; + encodedAmenities: Nat64 = 0; + encodedAmenities2: Nat64 = 0; + reviews = List.nil(); + }; + + /////////////////////////// Admins functions ///////////////////////////////// + + public shared ({ caller }) func addAdmin(p: Principal): async {#Ok; #Err} { + if(not isAdmin(caller)){ + #Err + } else{ + ignore Set.put(admins, phash, p); + #Ok + } + }; + + public shared ({ caller }) func removeAdmin(p: Principal): async {#Ok; #Err} { + if(caller != DEPLOYER){ + #Err; + } else { + ignore Set.remove(admins, phash, p); + #Ok + } + }; + public shared ({ caller }) func settings(): async ?Types.Settings{ + if(not isAdmin(caller)){ return null }; + ?{ + cancellationFeeCompensateBuyer = CancellationFeeCompensateBuyer; + reservationFee = ReservationFee; + timeToPay = TimeToPay; + minDaysBeforeCheckinForCancellation = MinDaysBeforeCheckinForCancellation; + } + }; + + public shared ({ caller }) func setMinter(m: Principal): async {#Ok; #Err: Text} { + if(not isAdmin(caller)) { return #Err(msg.NotAdmin) }; + TourMinterCanister := actor(Principal.toText(m)); + TourLedgerCanisterID := Principal.toText(await TourMinterCanister.getLedgerCanisterId()); + #Ok + }; + + public shared ({ caller }) func getMinterCanisterId(): async Principal { + if(not isAdmin(caller)) { return Principal.fromText(NULL_ADDRESS) }; + Principal.fromActor(TourMinterCanister); + }; + + public shared ({ caller }) func getUserTourBalance(subaccount: ?Blob): async Nat { + if(TourLedgerCanisterID != NULL_ADDRESS){ + let ledger = actor(TourLedgerCanisterID): actor { + icrc1_balance_of : shared {owner: Principal; subaccount: ?Blob} -> async Nat + }; + return await ledger.icrc1_balance_of({owner = caller; subaccount}); + }; + 0 + }; + + public shared ({ caller }) func setICPvTourRewardRatio(ratio: RewardRatio): async {#Ok; #Err}{ + if(not isAdmin(caller)) { return #Err }; + ICPvTourRewardRatio := ratio; + #Ok + }; + + public shared ({ caller }) func serRewardRatio(v: Nat64): async {#Ok; #Err}{ + if(not isAdmin(caller)) { return #Err }; + RewardRatio := v; + #Ok + }; + + public shared ({ caller }) func updateSettings(config: Types.Settings): async Bool{ + if(not isAdmin(caller)){ return false }; + CancellationFeeCompensateBuyer := config.cancellationFeeCompensateBuyer; + ReservationFee := config.reservationFee; + TimeToPay := config.timeToPay; + MinDaysBeforeCheckinForCancellation := config.minDaysBeforeCheckinForCancellation; + true + + }; + + /////////////////////////// Admin functions ////////////////////////////////////////////// + /////////////////////////////// Verification process ///////////////////////////////////// + // TODO actualmente todos los usuarios se inicializan como verificados + + // func userIsVerificated(u: Principal): Bool { + // let user = Map.get(users, phash, u); + // switch user{ + // case null { false }; + // case (?user) { user.verified}; + // }; + // }; + + //////////////////////////////// CRUD Housing //////////////////////////////////////////// + + public shared ({ caller }) func createHousing(dataInit: HousingCreateData): async {#Ok: Nat; #Err: Text} { + let hostUser = Map.get(hostUsers, phash, caller); + switch hostUser { + case null {#Err(msg.NotHostUser)}; + case (?hostUser) { + + lastHousingId += 1; + let newHousing: Housing = { + dataInit and + defaultHousinValues with + housingId = lastHousingId; + owner = caller; + calendary = {dayZero = now(); reservations = []}; + reservationsPending = []; + unavailability = { busy = []; pending = [] }; + }; + let housingIdsUser = List.push(lastHousingId, hostUser.housingIds); + ignore Map.put(hostUsers, phash, caller, {hostUser with housingIds = housingIdsUser}); + ignore Map.put(housings, nhash, lastHousingId, newHousing ); + #Ok(lastHousingId) + } + } + }; + + func isPublishable(housing: Housing): Bool { + (housing.price != null) and + (not addressEqual(housing.address, NULL_LOCATION)) and + (housing.properties != null) + }; + + public shared ({ caller }) func publishHousing(housingId: HousingId): async {#Ok; #Err: Text}{ + let hostUser = Map.get(hostUsers, phash, caller); + switch hostUser { + case null { #Err(msg.NotUser)}; + case ( ?hostUser ) { + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(housing.owner != caller) { return #Err(msg.CallerNotHousingOwner)}; + if(isPublishable(housing)) { + ignore Map.put(housings, nhash, housingId, {housing with active = true}); + #Ok + } else { + #Err(msg.IsNotpublishable) + } + } + } + } + } + + }; + + public shared ({ caller }) func addPhotoToHousing({id: HousingId; photo: Blob}): async {#Ok; #Err: Text} { + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + #Err(msg.NotHousing) + }; + case (?housing) { + if(housing.owner != caller){ + return #Err(msg.CallerNotHousingOwner) + }; + let photos = Prim.Array_tabulate( + housing.photos.size() +1, + func i = if(i < housing.photos.size()) {housing.photos[i]} else {photo} + ); + ignore Map.put(housings, nhash, id, {housing with photos}); + print(debug_show({housing with photos})); + #Ok + } + } + }; + + public shared ({ caller }) func addThumbnailToHousing({id: HousingId; thumbnail: Blob}): async {#Ok; #Err: Text} { + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + #Err(msg.NotHousing) + }; + case (?housing) { + if(housing.owner != caller){ + return #Err(msg.UnauthorizedCaller) + }; + ignore Map.put(housings, nhash, id, {housing with thumbnail}); + #Ok + } + } + }; + + public shared ({ caller }) func updatePrices({id: HousingId; price_: Types.Price}): async UpdateResult{ + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + return #Err(msg.NotHousing); + }; + case (?housing) { + if(housing.owner != caller){ return #Err(msg.UnauthorizedCaller) }; + ignore Map.put(housings, nhash, id, {housing with price = ?price_}); + return #Ok + }; + } + }; + + public shared ({ caller }) func setRulesForHousing({id: HousingId; rules: [Types.Rule]}): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + return #Err(msg.NotHousing); + }; + case (?housing) { + if(housing.owner != caller){ return #Err(msg.UnauthorizedCaller) }; + ignore Map.put(housings, nhash, id, {housing with rules}); + return #Ok + }; + } + }; + + public shared ({ caller }) func setMinReservationLeadTime({id: HousingId; hours: Nat}):async {#Ok; #Err: Text} { + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + return #Err(msg.NotHousing); + }; + case (?housing) { + if(housing.owner != caller){ + return #Err(msg.CallerNotHousingOwner); + }; + ignore Map.put( + housings, + nhash, + id, + {housing with minReservationLeadTimeNanoSeg = hours * 60 * 60 * 1_000_000_000}); + #Ok; + } + + } + }; + + public shared ({ caller }) func setHousingStatus({id: HousingId; active: Bool}): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, id); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + if (not isPublishable(housing)) { + return #Err(msg.IsNotpublishable) + }; + ignore Map.put(housings, nhash, id, {housing with active}); + #Ok + } + } + }; + + public shared ({ caller }) func setChekInCheckOut({housingId: HousingId; checkIn: Nat; checkOut: Nat}): async {#Ok; #Err: Text}{ + if(checkOut >= checkIn) { return #Err(msg.ErrorSetHoursCheckInCheckOut) }; + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + ignore Map.put(housings, nhash, housingId, {housing with checkIn; checkOut}); + #Ok + } + } + }; + + public shared ({ caller }) func setAddress({housingId: HousingId; address: Types.Location}): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + ignore Map.put(housings, nhash, housingId, {housing with address}); + #Ok + } + } + }; + + public shared ({ caller }) func locateOnTheMap({housingId: HousingId; lat: Int; lng: Int}): async {#Ok; #Err: Text} { // lat y lng van multiplicados por 10e7 + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + let address = {housing.address with geolocation = ?{lat; lng}}; + ignore Map.put(housings, nhash, housingId, {housing with address}); + #Ok + } + } + }; + + public shared ({ caller }) func cloneHousingWithProperties({housingId: HousingId; qty: Nat; housingTypeInit: HousingTypeInit}): async {#Ok; #Err: Text} { + let hostUser = Map.get(hostUsers, phash, caller); + switch hostUser { + case null { + return #Err(msg.NotHostUser) + }; + case ( ?hostUser ) { + if (Map.has(hostUser.housingTypes, thash, housingTypeInit.nameType)) { + return #Err(msg.HousingTypeExist) + }; + let housing = Map.get(housings, nhash, housingId); + switch housing { + case ( null ) { return #Err(msg.NotHousing)}; + case ( ?housing ) { + if(housing.owner != caller) { + return #Err(msg.CallerNotHousingOwner) + }; + //Establecemos el tipo al housing a clonar + let housingType: HousingType = {housingTypeInit with housingIds: [HousingId] = []}; + ignore Map.put( + housings, + nhash, + housingId, + { housing with properties = ?housingType; housingType = ?housingTypeInit.nameType}); + + var i = qty; + var housingIdsOfThisType = Buffer.fromArray([housingId]); + while (i > 0) { + lastHousingId += 1; + housingIdsOfThisType.add(lastHousingId); + + let newHousing: Housing = { + defaultHousinValues with + owner = caller; + housingId = lastHousingId; + namePlace = housing.namePlace; + nameHost = housing.nameHost; + descriptionPlace = housing.descriptionPlace; + descriptionHost = housing.descriptionHost; + link = housing.link; + photos = []; + properties = null; + housingType = ?housingTypeInit.nameType; + thumbnail = housing.thumbnail; + calendary = {dayZero = now(); reservations = []}; + reservationsPending = []; + unavailability = { busy = []; pending = [] }; + }; + ignore Map.put(housings, nhash, lastHousingId, newHousing ); + i -= 1; + }; + let housingIds = Buffer.toArray(housingIdsOfThisType); + ignore Map.put(hostUser.housingTypes, thash, housingType.nameType, {housingType with housingIds}); + #Ok + } + }; + } + }; + }; + + + // public shared ({ caller }) func removeHousingType(housingType: Text): async {#Ok; #Err: Text}{ + // let myHousingTypesMap = Map.get(housingTypesByHostOwner, phash, caller); + // switch myHousingTypesMap { + // case null {#Err("Not housing types")}; + // case (?housingTypesMap){ + // let removedType = Map.remove( + // housingTypesMap, thash, housingType + // ); + // switch removedType { + // case null { #Err("Not housing type")}; + // case (?removedType) {#Ok} + // } + // } + // } + // }; + + public shared ({ caller }) func setAmenities(amenities: Types.Amenities, housingId: HousingId): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + let encodedAmenities = encodeAmenities(amenities); + ignore Map.put( + housings, + nhash, + housingId, + {housing with amenities = ?amenities; encodedAmenities}); + #Ok + } + } + }; + + ///// Prueba + + public shared ({ caller }) func setAmenities2(housingId: Nat, encodedAmenities2: Nat64): async {#Ok; #Err: Text} { + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + ignore Map.put( + housings, + nhash, + housingId, + {housing with encodedAmenities2}); + #Ok + } + } + }; + + ///////////////////////////////////////// Getters //////////////////////////////////////// + + public query func getHousingPaginate({page: Nat; qtyPerPage: Nat}): async ResultHousingPaginate { + if(Map.size(housings) < page * qtyPerPage){ + return #Err(msg.PaginationOutOfRange) + }; + let values = Map.toArray(housings); + let bufferHousingPreview = Buffer.fromArray([]); + var index = page * qtyPerPage; + while (index < values.size() and index < (page + 1) * qtyPerPage){ + if(values[index].1.active){ + bufferHousingPreview.add(values[index].1); + }; + index += 1; + }; + let array = Buffer.toArray(bufferHousingPreview); + #Ok{ + array; + hasNext = ((page + 1) * qtyPerPage < array.size()) + } + }; + + public query func filterByAmenities({filterCode: Nat64; page: Nat; qtyPerPage: Nat}): async ResultHousingPaginate { + let filteredHosuings = Array.filter( + Iter.toArray(Map.vals(housings)), + func h = h.active and ((h.encodedAmenities & filterCode) == filterCode) + ); + if(filteredHosuings.size() < page * qtyPerPage){ + return #Err(msg.PaginationOutOfRange) + }; + let (size: Nat, hasNext: Bool) = if (filteredHosuings.size() >= (page + 1) * qtyPerPage){ + (qtyPerPage, filteredHosuings.size() > (page + 1)) + } else { + (filteredHosuings.size() % qtyPerPage, false) + }; + let array = Array.subArray(filteredHosuings, page * qtyPerPage, size); + #Ok{ array; hasNext } + }; + + // public query func filterByProperties({filterCode: Nat64; page: Nat; qtyPerPage: Nat}): async ResultHousingPaginate { + // let filteredHosuings = Array.filter( + // Iter.toArray(Map.vals(housings)), + // func h = h.active and ((h.encodedAmenities & filterCode) == filterCode) + // ); + // if(filteredHosuings.size() < page * qtyPerPage){ + // return #Err(msg.PaginationOutOfRange) + // }; + // let (size: Nat, hasNext: Bool) = if (filteredHosuings.size() >= (page + 1) * qtyPerPage){ + // (qtyPerPage, filteredHosuings.size() > (page + 1)) + // } else { + // (filteredHosuings.size() % qtyPerPage, false) + // }; + // let array = Array.subArray(filteredHosuings, page * qtyPerPage, size); + // #Ok{ array; hasNext } + // }; + + + public shared ({ caller }) func getCalendarById(id: Nat): async {#Ok: Calendary; #Err: Text}{ + switch (Map.get(housings, nhash, id)) { + case (?housing) { + if (housing.owner != caller ) {return #Err(msg.CallerNotHousingOwner)}; + let { calendary } = updateCalendary(id); + #Ok(calendary) + }; + case null { return #Err(msg.NotHousing)}; + }; + }; + + public shared ({ caller }) func getHousingById({housingId: HousingId; photoIndex: Nat}): async {#Ok: HousingResponse; #Err: Text} { + let housing = Map.get(housings, nhash, housingId); + + return switch housing { + case null { #Err(msg.NotHousing)}; + case (?housing) { + let reservationsPending = cleanPendingVerifications(housingId); + if(not housing.active and housing.owner != caller) { + return #Err(msg.InactiveHousing) + }; + let { unavailability } = updateCalendary(housingId); + if(photoIndex == 0){ + let housingResponse: HousingResponse = #Start({ + housing with + reservationsPending; + calendary = {dayZero = 0 ; reservations = []}; //Informacion omitida para el publico + unavailability; + photos = if(housing.photos.size() > 0) { [housing.photos[0]] } else { [] }; + hasNextPhoto = (housing.photos.size() > photoIndex + 1) + }); + #Ok(housingResponse); + } else { + if (photoIndex >= housing.photos.size()) { return #Err(msg.PaginationOutOfRange)}; + let housingResponse: HousingResponse = #OnlyPhoto({ + photo = housing.photos[photoIndex]; + hasNextPhoto = (housing.photos.size() > photoIndex + 1) + }); + print(debug_show(housing.photos)); + #Ok(housingResponse) + } + }; + } + }; + + public shared ({ caller }) func getMyHousingTypes(): async {#Ok: [{typeName: Text; housingIds: [Nat]}]; #Err: Text}{ + let hostUser = Map.get(hostUsers, phash, caller); + switch hostUser { + case null {#Err(msg.NotHostUser)}; + case (?hostUser) { + #Ok( + Array.map<(Text, HousingType),{ typeName: Text; housingIds: [Nat]} >( + Map.toArray(hostUser.housingTypes), + func x = {typeName = x.0; housingIds = x.1.housingIds} + ) + ) + }; + } + }; + + public shared query ({ caller }) func getMyHousingsByType({housingType: Text; page: Nat}): async ResultHousingPaginate{ + let hostUser = Map.get(hostUsers, phash, caller); + switch hostUser { + case null {#Err(msg.NotHostUser)}; + case (?hostUser) { + switch (Map.get(hostUser.housingTypes, thash, housingType)){ + case null { return #Err(msg.HousingTypeNoExist)}; + case ( ?housingType ) { + getPaginateHousings(housingType.housingIds, page) + } + } + }; + } + }; + + func getPaginateHousings(ids: [HousingId], page: Nat): ResultHousingPaginate { + let resultBuffer = Buffer.fromArray([]); + var index = page * 10; + while(index < (page + 1)* 10 and index < ids.size()){ + switch (Map.get(housings, nhash, ids[index])) { + case null {}; + case (?housing) { + resultBuffer.add( + { + active = housing.active; + housingId = ids[index]; + address = housing.address; + thumbnail = housing.thumbnail; + price = housing.price; + encodedAmenities = housing.encodedAmenities + } + ) + } + }; + index += 1; + }; + let hasNext = ids.size() > (page + 1)* 10; + #Ok({array = Buffer.toArray(resultBuffer); hasNext: Bool}); + }; + + func getHousingsPaginateByOwner(owner: Principal, page: Nat, qtyPerPage: Nat, onlyActives: Bool): ResultHousingPaginate { + let hostUser = Map.get(hostUsers, phash, owner); + + switch hostUser { + case null { #Err(msg.NotHostUser)}; + case ( ?hostUser ) { + // let housingArray = List.toArray(hostUser.housingIds); + let housingPreviewBuffer = Buffer.fromArray([]); + for(id in List.toIter(hostUser.housingIds)){ + switch (Map.get(housings, nhash, id)){ + case (?housing) { + if(not onlyActives or housing.active){ + housingPreviewBuffer.add(housing) + } + }; + case _ { } + }; + }; + let arrayHousingPreview = Buffer.toArray(housingPreviewBuffer); + if ( arrayHousingPreview.size() < page * qtyPerPage){ + return #Err(msg.PaginationOutOfRange); + }; + let (size: Nat, hasNext: Bool) = if (arrayHousingPreview.size() >= (page + 1) * qtyPerPage){ + (qtyPerPage, arrayHousingPreview.size() > (page + 1)) + } else { + (arrayHousingPreview.size() % qtyPerPage, false) + }; + return #Ok({array = Array.subArray(arrayHousingPreview, page * qtyPerPage, size); hasNext : Bool}) + + } + } + }; + + public shared query ({ caller }) func getMyHousingsPaginate({page: Nat; qtyPerPage: Nat}): async ResultHousingPaginate{ + getHousingsPaginateByOwner(caller, page, qtyPerPage, false) + }; + + public shared query ({ caller }) func getMyActiveHousings({page: Nat; qtyPerPage: Nat}): async ResultHousingPaginate{ + getHousingsPaginateByOwner(caller, page, qtyPerPage, true) + }; + + func encodeAmenities(a: Types.Amenities): Nat64 { + var result = 0: Nat64; + // El orden de los elementos de este array tiene que coincidir con el array para la decodificacion + let arrayBools = [ + a.freeWifi, + a.airCond, + a.flatTV, + a.minibar, + a.safeBox, + a.roomService, + a.premiumLinen, + a.ironBoard, + a.privateBath, + a.hairDryer, + a.hotelRest, + a.barLounge, + a.buffetBrkfst, + a.lobbyCoffee, + a.catering, + a.specialMenu, + a.outdoorPool, + a.spaWellness, + a.gym, + a.jacuzzi, + a.gameRoom, + a.tennisCourt, + a.natureTrails, + ]; + for(i in arrayBools.vals()) { result := result * 2 + (if i { 1 } else { 0 })}; + result + // Ejemplo aproximado para llenar array de Booleanos a partir de result y el array de amenidades equivalente en el front: + // let amenities = ["freeWifi", "airCond" ... etcetera] + // let amenitiesTrue = []; + // for (let i = 0; i < amenities.length; i++) { + // if ((encodedAmenities & (1 << amenities.length - 1 - i) != 0)){ + // amenitiesTrue.push(amenities[i]); + // } + // } + // //Ejemplo para filtar por jacuzzi + // let jacuzzi = (encodedAmenities >> (amenities.length - 19 -1)) % 2 === 1; + // //Ejemplo para filtrar por jasuzzi y minibar: + // let jacuzziYMiniBar = (encodedAmenities & (1 << amenities.length - 1 - 20) != 0) and + // (encodedAmenities & (1 << amenities.length - 1 - 3) != 0); + + }; + + // public func filterHousing(filters: Filter) { + + // }; + + public shared query func getHousingByHostUser({host: Principal; page: Nat; qtyPerPage: Nat}): async ResultHousingPaginate{ + getHousingsPaginateByOwner(host, page, qtyPerPage, true) + }; + + type UpdateCalendaryResponse = { + calendary: Calendary; + unavailability: {busy: [Int]; pending: [Int]}; + }; + + func updateCalendary(housingId: HousingId): UpdateCalendaryResponse{ + switch (Map.get(housings, nhash, housingId)){ + case null { + assert false; // La siguiente linea no se ejecuta nunca pero se requiere que todas las ramificaciones tengan una ultima linea con una expresion del tipo de retorno + { calendary = {dayZero = 0; reservations = []} ; unavailability = {busy = []; pending = []}}; + }; + case ( ?housing ) { + let startOfCurrentDayGMT = now() - now() % nanoSecPerDay ; // Timestamp inicio del dia actual en GTM + 0 + let daysSinceLastUpdate = (startOfCurrentDayGMT - housing.calendary.dayZero) / nanoSecPerDay; + // El siguiente bloque funciona bien pero se puede acomodar mejor + if(daysSinceLastUpdate > 0){ + var updateArray = Array.filter( + housing.calendary.reservations, + func(x: Reservation): Bool {x.checkOut >= daysSinceLastUpdate} + ); + updateArray := Prim.Array_tabulate( + updateArray.size(), + func x {{ + updateArray[x] with + checkIn = updateArray[x].checkIn - daysSinceLastUpdate; + checkOut = updateArray[x].checkOut - daysSinceLastUpdate + }} + ); + let busy = extractDaysNotAvailable(#ReservationType(updateArray)); + let pending = extractDaysNotAvailable(#IdsReservation(housing.reservationsPending)); + let unavailability = {busy; pending}; + let calendary = {dayZero = startOfCurrentDayGMT; reservations = updateArray}; + let housingUpdate = {housing with calendary; unavailability}; + ignore Map.put(housings, nhash, housingId, housingUpdate); + return {calendary; unavailability}; + } else { + let busy = extractDaysNotAvailable(#ReservationType(housing.calendary.reservations)); + let pending = extractDaysNotAvailable(#IdsReservation(housing.reservationsPending)); + let unavailability = {busy; pending}; + return {calendary = housing.calendary; unavailability } + }; + } + } + }; + + func extractDaysNotAvailable(input: {#ReservationType: [Reservation]; #IdsReservation : [Nat]}): [Int] { + let bufferDaysUnavailable = Buffer.fromArray([]); + let reservationsArray = switch input { + case ( #ReservationType(reservationsArray)){ reservationsArray }; + case ( #IdsReservation(idsArray) ) { + let bufferReservations = Buffer.fromArray([]); + for (id in idsArray.vals()) { + switch (Map.get(reservationsPendingConfirmation, nhash, id)) { + case null { }; + case (?reservation) { bufferReservations.add(reservation)} + }; + }; + Buffer.toArray(bufferReservations); + } + }; + for (r in reservationsArray.vals()) { + var dayOccuped = r.checkIn; + while(dayOccuped < r.checkOut ) { + bufferDaysUnavailable.add(dayOccuped); + dayOccuped += 1; + } + }; + Array.sort(Buffer.toArray(bufferDaysUnavailable), Int.compare); + + + }; + + func cleanPendingVerifications(housingId: Nat): [Nat] { //Remueve las solicitudes no confirmadas y con el tiempo de confirmacion transcurrido; + // TODO Esta funcion viola los principios solid ya que se encarga de limpiar las solicitudes pendientes tanto del Map general + // como tambien los id de solicitudes de dentro de las estructuras de los housing y ademas devuelve un array con los ids vigentes + // correspondientes al HousingID pasado por parametro. Ealuar alguna refactorizacion + // var reservationsPendingForId = ids; + var reservationsPendingForTheProvidedID: [Nat] = []; + for ((id, reservation) in Map.toArray(reservationsPendingConfirmation).vals()) { + if (now() > reservation.date + TimeToPay) { + // print("Solicitud de reserva " # Nat.toText(id) # " Eliminada por timeout"); + ignore Map.remove(reservationsPendingConfirmation, nhash, id); + let housing = Map.get(housings, nhash, reservation.housingId); + switch housing { + case null { }; + case (?housing) { + let reservationsPending = Array.filter( + housing.reservationsPending, + func x = x != id + ); + // print("Id de solicitud " # Nat.toText(id) # " borrado\nreservas pendientes: "); + // print(debug_show(reservationsPending)); // revisar el filter + if (id == housingId ) { reservationsPendingForTheProvidedID := reservationsPending }; + ignore Map.put(housings, nhash, reservation.housingId, {housing with reservationsPending}); + } + } + } + }; + reservationsPendingForTheProvidedID + }; + + func getPendingReservations(ids: [Nat]): {pendings: [Reservation]; pendingReservUpdate: [Nat]}{ + let bufferReservations = Buffer.fromArray([]); + let bufferReservUpdate = Buffer.fromArray([]); + for (id in ids.vals()) { + switch (Map.get(reservationsPendingConfirmation, nhash, id)) { + case null {bufferReservUpdate.add(id)}; + case ( ?reservation ) { + // Si la solicitud es antigua se elimina del map y se devuelte el id para limpiar + if ( now() > reservation.date + TimeToPay) { + ignore Map.remove(reservationsPendingConfirmation, nhash, id); + // print("reserva " # Nat.toText(id) # " eliminada"); + } else { + bufferReservations.add(reservation); + bufferReservUpdate.add(id); + } + }; + }; + }; + {pendings = Buffer.toArray(bufferReservations); pendingReservUpdate = Buffer.toArray(bufferReservUpdate)} + }; + + func checkDisponibility(housingId: HousingId, chechIn: Nat, checkOut: Nat): Bool { + switch (Map.get(housings, nhash, housingId)) { + case (?housing) { + if(not housing.active) { return false } + else { + // let updatedCalendary = updateCalendary(housing.calendary); + let { calendary } = updateCalendary(housingId); + var checkDay = chechIn; + let {pendings; pendingReservUpdate} = getPendingReservations(housing.reservationsPending); + ignore Map.put(housings, nhash, housingId, {housing with reservationsPending = pendingReservUpdate}); + while (checkDay < checkOut) { + for (occuped in calendary.reservations.vals()){ + if(checkDay >= occuped.checkIn and checkDay < occuped.checkOut) { + return false; + }; + }; + if (pendingReservUpdate.size() > 0) { + print("Hay " # Nat.toText(pendingReservUpdate.size()) # " reservations pendientes"); + for (bloqued in pendings.vals()){ + print(debug_show(bloqued)); + if(checkDay >= bloqued.checkIn and checkDay < bloqued.checkOut) { + return false; + }; + } + }; + checkDay += 1; + }; + return true + } + }; + case _ { return false } + }; + }; + ////////// view reservations pendding ///////////////// + public query func pendingReserv(): async () { + print(debug_show(Map.toArray(reservationsPendingConfirmation))) + }; + + ////////////////////////////////////////////////////// + public shared query ({ caller }) func getMyHousingDisponibility({checkIn: Nat; checkOut: Nat; page: Nat; qtyPerPage: Nat}): async ResultHousingPaginate{ + let response = getHousingsPaginateByOwner(caller, page, qtyPerPage, true); + switch response { + case (#Ok({array; hasNext} )){ + let bufferResults = Buffer.fromArray([]); + for(hostPreview in array.vals()){ + if(checkDisponibility(hostPreview.housingId, checkIn, checkOut)){ + bufferResults.add(hostPreview); + }; + }; + #Ok({array = Buffer.toArray(bufferResults); hasNext}) + }; + case (#Err(msg)) { #Err(msg) } + } + }; + + public query func getAmenities({housingId: HousingId}): async ?Types.Amenities { + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { null }; + case (?housing) { + housing.amenities + } + } + }; + + ///////////////////////////// Reservations //////////////////////////// + + func blobToText(t: Blob): Text { + var result = ""; + let chars = ["0", "1" , "2" , "3" ,"4" , "5" , "6" , "7" , "8" , "9" , "a" , "b" , "c" , "d" , "e" , "f"]; + for (c in Blob.toArray(t).vals()){ + result #= chars[Nat8.toNat(c) / 16]; + result #= chars[Nat8.toNat(c) % 16] + }; + result + }; + + func calculatePrice(price: ?Types.Price, daysInt: Int): Nat64 { + switch price { + case null {assert (false); 0 }; + case (?price) { + var currentDiscount = 0; + let days = Int.abs(daysInt); + let discounts = Array.sort<{minimumDays: Nat; discount: Nat}>( + price.discountTable, + func (a, b) = if (a.minimumDays < b.minimumDays) { #less } else { #greater } + ); + for(discount in discounts.vals()){ + // print(debug_show(discount)); + if(days < discount.minimumDays) { return Nat64.fromNat((price.base * days) - (price.base * days * currentDiscount / 100)) }; + currentDiscount := discount.discount; + }; + return Nat64.fromNat((price.base * days) - (price.base * days * currentDiscount / 100)); + } + } + }; + + func putRequestReservationToHousing(housingId: HousingId, requestId: Nat) { + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null {assert false}; + case ( ?housing ) { + let reservationsPending = Prim.Array_tabulate( + housing.reservationsPending.size() + 1, + func x = if( x == 0 ) {requestId} else {housing.reservationsPending[x - 1]} + ); + ignore Map.put(housings, nhash, housingId, {housing with reservationsPending}); + } + } + }; + + public shared ({ caller = requester }) func requestReservation(data: Types.ReservationDataInput): async TransactionResponse { + let {housingId; guest; email; phone; checkIn; checkOut}: Types.ReservationDataInput = data; + let housing = Map.get(housings, nhash, housingId); + if ( not Map.has(users, phash, requester) ) { + return #Err(msg.NotUser) + }; + if ( checkIn >= checkOut or checkIn < 0) { + return #Err(msg.ErrorSetHoursCheckInCheckOut) + }; + + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(checkDisponibility(housingId, Prim.abs(checkIn), Prim.abs(checkOut))){ + lastReservationId += 1; + let amount = calculatePrice(housing.price, Prim.abs(checkOut - checkIn)); + let reservation: Reservation = { + date = now(); + timestampCheckIn = (now() - now() % nanoSecPerDay) + (checkIn * nanoSecPerDay) /* + (housing.checkIn * 60 * 60 * 1000000000) */; + housingId; + reservationId = lastReservationId; + requester; + ownerHousing = housing.owner; + checkIn; + checkOut; + guest; + email; + phone; + // confirmated = false; + status = #Pending; + amount; + dataTransaction = Types.NullTrx; + }; + ignore Map.put(reservationsPendingConfirmation, nhash, lastReservationId, reservation); + putRequestReservationToHousing(housingId, lastReservationId); + + let dataTransaction: TransactionParams = { + // Se toma el account por defecto correspondiente al principal del dueño del Host + // Se puede establecer otro account proporcionado por el usuario + to = blobToText(AccountIdentifier.accountIdentifier(housing.owner, AccountIdentifier.defaultSubaccount())); + amount; + }; + #Ok({transactionParams = dataTransaction; reservationId = lastReservationId}); + } else { + #Err(msg.NotAvalableAllDays) + }; + } + } + }; + + func verifyTransaction({from; to; amount}: DataTransaction, registeredAmount: Nat64): async Bool { + // TODO Verificar tambien aca + return true; // Test + if(amount != registeredAmount) { return false }; + let indexer_icp = actor("qhbym-qaaaa-aaaaa-aaafq-cai"): actor { + get_account_identifier_transactions : + shared query Indexer_icp.GetAccountIdentifierTransactionsArgs -> async Indexer_icp.GetAccountIdentifierTransactionsResult; + }; + let result = await indexer_icp.get_account_identifier_transactions({max_results = 10; start = null; account_identifier = from}); + switch result { + case (#Ok(response)) { + for (transaction in response.transactions.vals()) { + let operation = transaction.transaction.operation; + switch operation { + case( #Transfer(tx)) { + if (tx.from == from and + tx.to == to and + tx.amount.e8s >= amount){ + return true + } + }; + case ( _ ) { } + }; + }; + false + }; + case (#Err(_)) { false } + } + }; + + func calculateReward(amount: Nat64, coin: Text): async Nat64 { + let urlApi = "https://api3.binance.com/api/v3/ticker/price?symbol=" # Text.toUppercase(coin) # "USDT"; + //TODO convertir amount al equivalente en usdt + 1 * amount / RewardRatio; + }; + + public shared ({ caller }) func confirmReservation({reservationId: Nat; txData: DataTransaction}): async {#Ok: Reservation; #Err: Text} { + let reservation = Map.remove(reservationsPendingConfirmation, nhash, reservationId); + switch reservation { + case null { #Err(msg.NotReservation)}; + case ( ?reservation ){ + if (await verifyTransaction(txData, reservation.amount)){ + if(reservation.requester != caller) { + return #Err(msg.CallerIsNotRequester # Nat.toText(reservationId)) + }; + + let housing = Map.get(housings, nhash, reservation.housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + let currentReservation = { + reservation with + status = #Confirmed; + dataTransaction = {txData with amount = reservation.amount} + }; + let calendary = { + housing.calendary with + reservations = Prim.Array_tabulate( + housing.calendary.reservations.size() + 1, + func x = if (x == 0) { currentReservation } else { housing.calendary.reservations[x - 1] }) + }; + // print(debug_show(calendary)); + let reservationsPending = Array.filter(housing.reservationsPending, func x = x !=reservationId ); + ignore Map.put(reservationsHistory, nhash, reservationId, currentReservation); + ignore Map.put( + housings, + nhash, + reservation.housingId, + { housing with calendary; reservationsPending } + ); + ///// Rewards for confirm reservation //////////////////////////////////////////////////// + // TODO calcular recompenza en base al aquialente en dolares de reservation.amount + if (Principal.fromActor(TourMinterCanister) != Principal.fromText(NULL_ADDRESS)){ + let rewardAmount = await calculateReward(reservation.amount, "ICP"); + print("Mintenado recompenza: " # debug_show(rewardAmount) # " Tour"); + let accounts = [ + {owner = caller; subaccount = null }, + {owner = housing.owner; subaccount = null} + ]; + ignore await TourMinterCanister.issueRewards({accounts; amount = rewardAmount }) + }; + ////////////////////////////////////////////////////////////////////////////////////////// + #Ok(currentReservation) + } + } + } else { + #Err(msg.TransactionNotVerified) + }; + } + } + }; + + public shared ({ caller }) func requestToCancelReservation(reservationId: Nat): async TransactionResponse { + // TODO Actualizar el estado de las reservas en general, antes de proceder + let reservation = Map.get(reservationsHistory, nhash, reservationId); + switch reservation { + case null { return #Err(msg.NotReservation)}; + case ( ?reservation ) { + if (reservation.ownerHousing != caller) { + return #Err(msg.CallerNotHousingOwner # " corresponding to reservation # " # Nat.toText(reservationId)) + }; + if (reservation.status == #Confirmed) { + if(reservation.timestampCheckIn <= now() + MinDaysBeforeCheckinForCancellation * nanoSecPerDay){ + return #Err("The reservation cannot be cancelled less than " # Nat.toText(MinDaysBeforeCheckinForCancellation) # " days"); + }; + let dataTransaction: TransactionParams = { + to = reservation.dataTransaction.from; + amount = reservation.dataTransaction.amount + (reservation.dataTransaction.amount * CancellationFeeCompensateBuyer) / 100; + }; + #Ok({transactionParams = dataTransaction; reservationId = reservationId}) + } else { + let status = switch (reservation.status) { + case (#Pending) { " Pending Reservation" }; + case (#Canceled) { " Canceled " }; + case ( #Ended ) { " Ended " }; + case _ {""} + }; + #Err("Reservation status is " # status) + } + } + }; + }; + + public shared ({ caller }) func confirmCancelReservation({reservationId: Nat; txData: DataTransaction}): async {#Ok: Reservation; #Err: Text}{ + let reservation = Map.get(reservationsHistory, nhash, reservationId); + switch reservation { + case null { return #Err(msg.NotReservation)}; + case ( ?reservation ) { + if (reservation.ownerHousing != caller) { + return #Err(msg.CallerNotHousingOwner # " corresponding to reservation # " # Nat.toText(reservationId)) + }; + if (reservation.status != #Confirmed) { + return #Err("Reservation status is not confirmed") + }; + let amountWithFee = reservation.dataTransaction.amount + (reservation.dataTransaction.amount * CancellationFeeCompensateBuyer) / 100; + if (await verifyTransaction(txData, amountWithFee)) { + ignore Map.put(reservationsHistory, nhash, reservationId, {reservation with status = #Canceled}); + let housing = Map.get(housings, nhash, reservation.housingId); + switch housing { + case null { return #Err(msg.NotHousing)}; + case ( ?housing ) { + let reservationsPending = Array.filter(housing.reservationsPending, func x = x != reservationId); + ignore Map.put( + housings, + nhash, + reservation.housingId, + { housing with reservationsPending } + ); + #Ok({reservation with status = #Canceled}) + } + }; + } else { + #Err(msg.TransactionNotVerified) + } + } + }; + }; + + public shared ({ caller }) func getReservationByDay(housingId: Nat, day: Nat): async {#Ok: Reservation; #Err: Text}{ + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { return #Err(msg.NotHousing)}; + case ( ?housing ) { + //// Actualización del calendario //// + let { calendary } = updateCalendary(housingId); + // ignore Map.put(housings, nhash, housingId, {housing with calendary; unavailability}); + ///////////////////////////////////// + if (housing.owner != caller) { return #Err(msg.CallerNotHousingOwner)}; + for (reservation in calendary.reservations.vals()) { + if (day >= reservation.checkIn and day < reservation.checkOut) { + return #Ok(reservation) + } + }; + return #Err("No reservation for this day") + } + } + }; +}; + diff --git a/backend/mainOld.mo.txt b/backend/mainOld.mo.txt new file mode 100644 index 0000000..c0bafa2 --- /dev/null +++ b/backend/mainOld.mo.txt @@ -0,0 +1,907 @@ +import Prim "mo:⛔"; +import Map "mo:map/Map"; +import Set "mo:map/Set"; +import { phash; nhash; thash } "mo:map/Map"; +import Principal "mo:base/Principal"; +import Buffer "mo:base/Buffer"; +import Blob "mo:base/Blob"; +import Types "types"; +// import Int "mo:base/Int"; +import { now } "mo:base/Time"; +// import Rand "mo:random/Rand"; +import msg "constants"; + +////////////// Debug imports //////////////// +import { print } "mo:base/Debug"; +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Int "mo:base/Int"; + +shared ({ caller }) actor class Triourism () = this { + + type User = Types.User; + type UserKind = Types.UserKind; + type SignUpResult = Types.SignUpResult; + // type CalendaryPart = Types.CalendaryPart; + type Calendary = {dayZero: Int; reservedDays: [Int]}; + type ReservationDataInput = Types.ReservationDataInput; + type Reservation = Types.Reservation; + type HousingId = Types.HousingId; + // type HousingDataInit = Types.HousingDataInit; + type Housing = Types.Housing; + type HousingResponse = Types.HousingResponse; + type HousingPreview = Types.HousingPreview; + type HousingCreateData = Types.HousingCreateData; + type HousingTypesMap = Map.Map; + + type UpdateResult = Types.UpdateResult; + type ResultHousingPaginate = {#Ok: {array: [HousingPreview]; hasNext: Bool}; #Err: Text}; + type PublishResult = {#Ok: HousingId; #Err: Text}; + + type ReservationResult = { + #Ok: { + reservationId: Nat; + msg: Text; + }; + #Err: Text; + }; + + // TODO revisar day, actualemnte es el timestamp de una hora especifica que delimita el comienzo del dia + + + stable let DEPLOYER = caller; + // let NANO_SEG_PER_HOUR = 60 * 60 * 1_000_000_000; + // let ramdomGenerator = Rand.Rand(); + + // stable var minReservationLeadTime = 24 * 60 * 60 * 1_000_000_000; // 24 horas en nanosegundos + + stable let admins = Set.new(); + ignore Set.put(admins, phash, caller); + stable let users = Map.new(); + stable let housings = Map.new(); + stable let housingTypesByHostOwner = Map.new(); + stable let calendars = Map.new(); + stable let reservationsRequests = Map.new(); + stable let reservationsConfirmed = Map.new(); + + stable var lastHousingId = 0; + + + ///////////////////////////////////// Update functions //////////////////////////////////////// + + private func _safeSignUp(p: Principal, data: Types.SignUpData, kind: UserKind): SignUpResult { + if(Principal.isAnonymous(p)){ + return #Err(msg.NotUser) + }; + let user = Map.get(users, phash, p); + switch user { + case (?User) { #Err("User already exists") }; + case null { + let newUser: User = { + data with + kinds: [UserKind] = [kind]; + verified = true; + score = 0; + }; + ignore Map.put(users, phash, p, newUser); + #Ok(newUser); + }; + }; + }; + + public shared ({ caller }) func signUp(data: Types.SignUpData) : async SignUpResult { + _safeSignUp(caller, data, #Initial) + }; + + public shared ({ caller }) func signUpAsHost(data: Types.SignUpData) : async SignUpResult { + _safeSignUp(caller, data, #Host([])); + + }; + + public shared query ({ caller }) func logIn(): async {#Ok: User; #Err} { + let user = Map.get(users, phash, caller); + switch user { + case null { #Err }; + case ( ?u ) { #Ok(u)} + }; + }; + + //////////////////////////////// CRUD Data User /////////////////////////////////// + + public shared ({ caller }) func editProfile(data: Types.SignUpData): async {#Ok; #Err}{ + let user = Map.get(users, phash, caller); + switch user { + case null { #Err }; + case (?user){ + ignore Map.put(users, phash, caller, { user with data}); + #Ok + }; + }; + }; + + + ///////////////////////////// Private functions /////////////////////////////////// + func isAdmin(p: Principal): Bool { Set.has(admins, phash, p) }; + + func isUser(p: Principal): Bool { Map.has(users, phash, p)}; + + func isHostUser(p: Principal): Bool { + let user = Map.get(users, phash, p); + switch user { + case null { false }; + case (?user) { + for (kind in user.kinds.vals()) { + switch kind { + case (#Host(_)) { return true }; + case _ {} + }; + }; + return false + } + } + }; + + func addIdToHostKind(arr: [Types.UserKind], id: HousingId): [Types.UserKind]{ + let bufferKinds = Buffer.fromArray([]); + for (k in arr.vals()) { + switch k { + case (#Host(ids)){ + let setIds = Set.fromIter(ids.vals(), nhash); + ignore Set.put(setIds, nhash, id); + bufferKinds.add(#Host(Set.toArray(setIds))) + }; + case (k) {bufferKinds.add(k)} + }; + }; + Buffer.toArray(bufferKinds); + }; + + func addressEqual(a: Types.Location, b: Types.Location ) : Bool { + a.country == b.country and + a.city == b.city and + a.neighborhood == b.neighborhood and + a.zipCode == b.zipCode and + a.externalNumber == b.externalNumber and + a.internalNumber == b.internalNumber + }; + + let NULL_LOCATION = { + country = ""; + city = ""; + neighborhood = ""; + zipCode = 0; street = ""; + externalNumber = 0; + internalNumber = 0 + }; + + let defaultHousinValues = { + active: Bool = false; + rules: [Types.Rule] = []; + price: ?Types.Price = null; + checkIn: Nat = 15; + checkOut: Nat = 12; + address: Types.Location = NULL_LOCATION; + properties: [Types.Property] = []; + amenities = null; + // calendar: [var Types.CalendaryPart] = [var]; + }; + + + + /////////////////////////// Manage admins functions ///////////////////////////////// + + public shared ({ caller }) func addAdmin(p: Principal): async {#Ok; #Err} { + if(not isAdmin(caller)){ + #Err + } else{ + ignore Set.put(admins, phash, p); + #Ok + } + }; + + public shared ({ caller }) func removeAdmin(p: Principal): async {#Ok; #Err} { + if(caller != DEPLOYER){ + #Err; + } else { + ignore Set.remove(admins, phash, p); + #Ok + } + }; + + /////////////////////////// Admin functions ////////////////////////////////////////////// + /////////////////////////////// Verification process ///////////////////////////////////// + // TODO actualmente todos los usuarios se inicializan como verificados + + // func userIsVerificated(u: Principal): Bool { + // let user = Map.get(users, phash, u); + // switch user{ + // case null { false }; + // case (?user) { user.verified}; + // }; + // }; + + //////////////////////////////// CRUD Housing //////////////////////////////////////////// + + + + + public shared ({ caller }) func createHousing(dataInit: HousingCreateData): async {#Ok: Nat; #Err: Text} { + let user = Map.get(users, phash, caller); + switch user { + case null {#Err(msg.NotUser)}; + case (?user) { + if (not isHostUser(caller)) { return #Err(msg.NotHostUser)}; + lastHousingId += 1; + + let newHousing: Housing = { + dataInit and + defaultHousinValues with + id = lastHousingId; + owner = caller; + }; + let kinds = addIdToHostKind(user.kinds, lastHousingId); + ignore Map.put(users, phash, caller, {user with kinds }); + ignore Map.put(housings, nhash, lastHousingId,newHousing ); + ignore Map.put(calendars, nhash, lastHousingId, {dayZero= now(); reservedDays= []}); + #Ok(lastHousingId) + } + } + }; + + func isPublishable(housing: Housing): Bool { + (housing.price != null) and + (not addressEqual(housing.address, NULL_LOCATION)) and + (housing.properties.size() > 0) + }; + + public shared ({ caller }) func publishHousing(housingId: HousingId): async {#Ok; #Err: Text}{ + let user = Map.get(users, phash, caller); + switch user { + case null { #Err(msg.NotUser)}; + case ( ?user ) { + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(isPublishable(housing)) { + ignore Map.put(housings, nhash, housingId, {housing with active = true}); + #Ok + } else { + #Err(msg.IsNotpublishable) + } + } + } + } + } + + }; + + public shared ({ caller }) func addPhotoToHousing({id: HousingId; photo: Blob}): async {#Ok; #Err: Text} { + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + #Err(msg.NotHousing) + }; + case (?housing) { + if(housing.owner != caller){ + return #Err(msg.CallerNotHousingOwner) + }; + let photos = Prim.Array_tabulate( + housing.photos.size() +1, + func i = if(i < housing.photos.size()) {housing.photos[i]} else {photo} + ); + ignore Map.put(housings, nhash, id, {housing with photos}); + print(debug_show({housing with photos})); + #Ok + } + } + }; + + public shared ({ caller }) func addThumbnailToHousing({id: HousingId; thumbnail: Blob}): async {#Ok; #Err: Text} { + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + #Err(msg.NotHousing) + }; + case (?housing) { + if(housing.owner != caller){ + return #Err(msg.UnauthorizedCaller) + }; + ignore Map.put(housings, nhash, id, {housing with thumbnail}); + #Ok + } + } + }; + + public shared ({ caller }) func updatePrices({id: HousingId; price_: Types.Price}): async UpdateResult{ + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + return #Err(msg.NotHousing); + }; + case (?housing) { + if(housing.owner != caller){ return #Err(msg.UnauthorizedCaller) }; + ignore Map.put(housings, nhash, id, {housing with price = ?price_}); + return #Ok + }; + } + }; + + public shared ({ caller }) func setRulesForHousing({id: HousingId; rules: [Types.Rule]}): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, id); + switch housing { + case null { + return #Err(msg.NotHousing); + }; + case (?housing) { + if(housing.owner != caller){ return #Err(msg.UnauthorizedCaller) }; + ignore Map.put(housings, nhash, id, {housing with rules}); + return #Ok + }; + } + }; + + // public shared ({ caller }) func setMinReservationLeadTime({id: HousingId; hours: Nat}):async {#Ok; #Err: Text} { + // let housing = Map.get(housings, nhash, id); + // switch housing { + // case null { + // return #Err(msg.NotHosting); + // }; + // case (?housing) { + // if(housing.owner != caller){ + // return #Err(msg.CallerNotHousingOwner); + // }; + // ignore Map.put( + // housings, + // nhash, + // id, + // {housing with minReservationLeadTimeNanoSeg = hours * NANO_SEG_PER_HOUR}); + // #Ok; + // } + + // } + // }; + + public shared ({ caller }) func setHousingStatus({id: HousingId; active: Bool}): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, id); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + if (not isPublishable(housing)) { + return #Err(msg.IsNotpublishable) + }; + ignore Map.put(housings, nhash, id, {housing with active}); + #Ok + } + } + }; + + public shared ({ caller }) func setChekInCheckOut({housingId: HousingId; checkIn: Nat; checkOut: Nat}): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + ignore Map.put(housings, nhash, housingId, {housing with checkIn; checkOut}); + #Ok + } + } + }; + + public shared ({ caller }) func setAddress({housingId: HousingId; address: Types.Location}): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + ignore Map.put(housings, nhash, housingId, {housing with address}); + #Ok + } + } + }; + + func createHousingType(user: Principal, propertiesOfType: Types.Property): {#Ok; #Err: Text} { + let housingTypesMap: HousingTypesMap = + switch(Map.get(housingTypesByHostOwner, phash, user)){ + case null {Map.new() }; + case ( ?map ) { map } + }; + switch (Map.get( + housingTypesMap, + thash, + propertiesOfType.nameType)) { + case null { + // Creación del nuevo tipo + ignore Map.put( + housingTypesMap, + thash, + propertiesOfType.nameType, + {properties = propertiesOfType; housingIds: [HousingId] = []} + ); + ignore Map.put(housingTypesByHostOwner, phash, user, housingTypesMap); + #Ok + }; + case (_) { + #Err(msg.HousingTypeExist) + } + } + }; + + func putHousingType(user: Principal, propertiesOfType: Types.Property, housingId: HousingId ){ + let housingTypesMap: HousingTypesMap = + switch(Map.get(housingTypesByHostOwner, phash, user)){ + case null {Map.new() }; + case ( ?map ) { map } + }; + let housingType: {properties: Types.Property; housingIds: [HousingId]} = + switch (Map.get( + housingTypesMap, + thash, + propertiesOfType.nameType)){ + case null {{properties = propertiesOfType; housingIds = [housingId]}}; + case (?housingType) { + let housingIds= Prim.Array_tabulate( + housingType.housingIds.size() + 1, + func x = if(x == 0) { housingId } else { housingType.housingIds[x - 1] } + ); + {properties = propertiesOfType; housingIds} + } + }; + ignore Map.put( + housingTypesMap, thash, propertiesOfType.nameType, housingType + ); + + }; + + public shared ({ caller }) func assignHousingType({housingId: HousingId; qty: Nat; propertiesOfType: Types.Property}): async {#Ok; #Err: Text} { + + let user = Map.get(users, phash, caller); + switch user { + case null {return #Err(msg.NotUser)}; + case ( ?user ) { + if (qty < 1) { return #Err(msg.ZeroIsNotAllowed)}; + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + let createResponse = createHousingType(caller, propertiesOfType); + switch createResponse { + case (#Ok) { + ignore Map.put(housings, nhash, housingId, {housing with properties = [propertiesOfType]}); + // agregamos la habitacion actual al nuevo tipo creado + putHousingType(caller, propertiesOfType, housingId); + // Clonacion de habitacion segun qty con valores por defecto para las nuevas + var index = 1; // la habitacion a partir de la que se define el tipo no se cuenta porque ya tiene su propio id + while (index < qty ){ + lastHousingId += 1; + let newHousing: Housing = { + housing with + id = lastHousingId; + active = false; + propertiesOfType + }; + putHousingType(caller, propertiesOfType, lastHousingId); + ignore Map.put(users, phash, caller, user ); + ignore Map.put(housings, nhash, lastHousingId, newHousing ); + + index += 1; + }; + #Ok + }; + case (#Err(msg)) {#Err(msg)} + }; + } + } + } + }; + }; + + public shared ({ caller }) func removeHousingType(housingType: Text): async {#Ok; #Err: Text}{ + let myHousingTypesMap = Map.get(housingTypesByHostOwner, phash, caller); + switch myHousingTypesMap { + case null {#Err("Not housing types")}; + case (?housingTypesMap){ + let removedType = Map.remove( + housingTypesMap, thash, housingType + ); + switch removedType { + case null { #Err("Not housing type")}; + case (?removedType) {#Ok} + } + } + } + }; + + public shared ({ caller }) func setAmenities(amenities: Types.Amenities, housingId: HousingId): async {#Ok; #Err: Text}{ + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { #Err(msg.NotHousing)}; + case ( ?housing ) { + if(caller != housing.owner) { + return #Err(msg.CallerNotHousingOwner); + }; + ignore Map.put( + housings, + nhash, + housingId, + {housing with amenities = ?amenities}); + #Ok + } + } + }; + + ///////////////////////////////////////// Getters //////////////////////////////////////// + + public query func getHousingPaginate({page: Nat; qtyPerPage: Nat}): async ResultHousingPaginate { + if(Map.size(housings) < page * qtyPerPage){ + return #Err(msg.PaginationOutOfRange) + }; + let values = Map.toArray(housings); + let bufferHousingPreview = Buffer.fromArray([]); + var index = page * qtyPerPage; + while (index < values.size() and index < (page + 1) * qtyPerPage){ + if(values[index].1.active){ + bufferHousingPreview.add(values[index].1); + }; + index += 1; + }; + #Ok{ + array = Buffer.toArray(bufferHousingPreview); + hasNext = ((page + 1) * qtyPerPage < values.size()) + } + }; + + public shared query ({ caller }) func getCalendarById(id: Nat): async {#Ok: Calendary; #Err: Text}{ + switch (Map.get(housings, nhash, id)) { + case (?housing) { + if(housing.active) { + switch (Map.get(calendars, nhash, id)) { + case null { return #Err(msg.NotHousing)}; + case (?calendar) { return #Ok(calendar) } + } + } else { + return #Err(msg.InactiveHousing) + } + }; + case null { return #Err(msg.NotHousing)}; + }; + + }; + + public query func getHousingById({housingId: HousingId; photoIndex: Nat}): async {#Ok: HousingResponse; #Err: Text} { + let housing = Map.get(housings, nhash, housingId); + return switch housing { + case null { #Err(msg.NotHousing)}; + case (?housing) { + if(not housing.active and housing.owner != caller) { + return #Err(msg.InactiveHousing) + }; + if(photoIndex == 0){ + let housingResponse: HousingResponse = #Start({ + housing with + photos = if(housing.photos.size() > 0) { [housing.photos[0]] } else { [] }; + hasNextPhoto = (housing.photos.size() > photoIndex + 1) + }); + #Ok(housingResponse); + } else { + let housingResponse: HousingResponse = #OnlyPhoto({ + photo = housing.photos[photoIndex]; + hasNextPhoto = (housing.photos.size() > photoIndex + 1) + }); + print(debug_show(housing.photos)); + #Ok(housingResponse) + } + }; + } + }; + + //TODO servicio que devuelva los tipos + + public shared query ({ caller }) func getMyHousingsByType({housingType: Text; page: Nat}): async ResultHousingPaginate{ + let housingTypeMap = Map.get(housingTypesByHostOwner, phash, caller); + switch housingTypeMap { + case null {#Err("Not Housing Types")}; + case (?map) { + let housingsType = Map.get( + map, + thash, + housingType + ); + switch housingsType { + case null { #Err("Not housing type")}; + case (?housingType) { + getPaginateHousings(housingType.housingIds, page) + } + } + }; + } + }; + + func getPaginateHousings(ids: [HousingId], page: Nat): ResultHousingPaginate { + let resultBuffer = Buffer.fromArray([]); + var index = page * 10; + while(index < (page + 1)* 10 and index < ids.size()){ + switch (Map.get(housings, nhash, ids[index])) { + case null {}; + case (?housing) { + resultBuffer.add( + { + active = housing.active; + id = ids[index]; + address = housing.address; + thumbnail = housing.thumbnail; + price = housing.price; + } + ) + } + }; + index += 1; + }; + let hasNext = ids.size() > (page + 1)* 10; + #Ok({array = Buffer.toArray(resultBuffer); hasNext: Bool}); + }; + + func getHousingsPaginateByOwner({owner: Principal; page: Nat}): ResultHousingPaginate { + let user = Map.get(users, phash, owner); + switch user { + case null { #Err("There is no user associated with the caller")}; + case ( ?user ) { + for( k in user.kinds.vals()){ + switch k { + case (#Host(hostIds)) { + let bufferHousingreview = Buffer.fromArray([]); + var index = page * 10; + while(index < hostIds.size() and index < 10 * (page + 1)){ + let housing = Map.get(housings, nhash, hostIds[index]); + switch housing { + case null{ }; + case ( ?housing ) { + let prev: HousingPreview = housing; + bufferHousingreview.add(prev); + } + }; + index += 1; + }; + return #Ok({array = Buffer.toArray(bufferHousingreview); hasNext = hostIds.size() > 10 * (page +1)}) + }; + case _ {}; + } + }; + #Err("The user is not a hosting type user") + } + } + }; + + public shared query ({ caller }) func getMyHousingsPaginate({page: Nat}): async ResultHousingPaginate{ + getHousingsPaginateByOwner({owner = caller; page}) + }; + + func updateCalendary(housingId: HousingId, calendary: Calendary): Calendary { + print("Actualizando calendario"); + let curranDay = now(); + let pastDays = (curranDay - calendary.dayZero) / (24 * 60 * 60 * 1_000_000_000); + print("Dias pasados: " # Int.toText(pastDays)); + if(pastDays > 0){ + var updateArray = Array.filter( + calendary.reservedDays, + func(x: Int): Bool {x > pastDays} + ); + updateArray := Prim.Array_tabulate( + updateArray.size(), + func (x: Nat) = updateArray[x] - pastDays + ); + let updateCalendar = {dayZero = curranDay; reservedDays = updateArray}; + ignore Map.put(calendars, nhash, housingId, updateCalendar); + return updateCalendar; + }; + calendary + }; + + func checkDisponibility(housingId: HousingId, chechIn: Int, checkOut: Int): Bool { + switch (Map.get(housings, nhash, housingId)) { + case (?housing) { if(not housing.active) { return false } }; + case _ { return false } + }; + switch (Map.get(calendars, nhash, housingId)) { + case null { false }; + case (?calendar) { + print("Calendario encontrado"); + let updatedCalendary = updateCalendary(housingId, calendar); + var checkDay = chechIn; + while (checkDay <= checkOut) { + for(occuped in updatedCalendary.reservedDays.vals()){ + if(checkDay == occuped) { + return false; + }; + }; + checkDay += 1; + }; + return true + } + } + }; + + public shared query ({ caller }) func getMyHousingDisponibility({checkIn: Nat; checkOut: Nat; page: Nat}): async ResultHousingPaginate{ + let response = getHousingsPaginateByOwner({owner = caller; page}); + switch response { + case (#Ok({array; hasNext} )){ + let bufferResults = Buffer.fromArray([]); + for(hostPreview in array.vals()){ + if(checkDisponibility(hostPreview.id, checkIn, checkOut)){ + bufferResults.add(hostPreview); + }; + }; + #Ok({array = Buffer.toArray(bufferResults); hasNext}) + }; + case (#Err(msg)) { #Err(msg) } + } + }; + + public query func getAmenities({housingId: HousingId}): async ?Types.Amenities { + let housing = Map.get(housings, nhash, housingId); + switch housing { + case null { null }; + case (?housing) { + housing.amenities + } + } + }; + + + // public shared query ({ caller }) func getReservations({housingId: Nat}): async {#Ok: [(Nat, Reservation)]; #Err: Text}{ + // let housing = Map.get(housings, nhash, housingId); + // switch housing { + // case null {#Err(msg.NotHousing)}; + // case ( ?housing ) { + // if(housing.owner != caller ){ + // return #Err(msg.CallerNotHousingOwner); + // }; + // #Ok(Map.toArray(housing.reservationRequests)) + // } + // } + // }; + + ///////////////////////////////// Reservations /////////////////////////////////////////// + + // public shared ({ caller }) func requestReservationOld({housingId: HousingId; data: ReservationDataInput}):async ReservationResult { + // let housing = Map.get(housings, nhash, housingId); + // switch housing { + // case null { + // #Err(msg.NotHosting); + // }; + // case (?housing) { + // print("housing"); + // /////// housing calendar update / housing Map update ////// + // let calendar = updateCalendar(housing.calendar); + // ignore Map.put(housings, nhash, housingId, {housing with calendar}); + + // ///////////////////////////////////////////////////// DEBUGIN ////////////////////////////////////////////////////////// + // print("Momento actual en NanoSeg: " # Int.toText(now())); + // print("horas de anticipacio: " # Int.toText(housing.minReservationLeadTimeNanoSec )); + // print("Reserva a partir de fecha: " # Int.toText((now() + housing.minReservationLeadTimeNanoSec))); + // print("Fecha de ingreso silicitada " # Int.toText(data.checkIn)); + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // if(now() + housing.minReservationLeadTimeNanoSec > data.checkIn){ + // return #Err("Reservations are requested at least " # + // Int.toText(housing.minReservationLeadTimeNanoSec /(NANO_SEG_PER_HOUR)) # + // " hours in advance."); + // }; + // if(availableAllDaysResquest(data.checkIn, data.checkOut)){ + // let reservationId = await ramdomGenerator.randRange(1_000_000_000, 9_999_999_999); + // let reservation = {data with applicant = caller}; + // let responseReservation = { + // reservationId; + // msg = "Espere" + // }; + // ignore Map.put(housing.reservationRequests, nhash, reservationId, reservation ); + // #Ok( responseReservation ) + // } else { + // #Err( msg.NotAvalableAllDays); + // } + // }; + // } + // }; + + // public shared ({ caller }) func requestReservation({housingId: HousingId; data: ReservationDataInput}): async ReservationResult{ + // let housing = Map.get(housings, nhash, housingId); + // switch housing { + // case null { + // #Err(msg.NotHosting); + // }; + // case (?housing) { + // print("housing"); + // /////// housing calendar update / housing Map update ////// + // let calendar = updateCalendar(housing.calendar); + // ignore Map.put(housings, nhash, housingId, {housing with calendar}); + + // ///////////////////////////////////////////////////// DEBUGIN ////////////////////////////////////////////////////////// + // print("Momento actual en NanoSeg: " # Int.toText(now())); + // print("horas de anticipacio: " # Int.toText(housing.minReservationLeadTimeNanoSec )); + // print("Reserva a partir de fecha: " # Int.toText((now() + housing.minReservationLeadTimeNanoSec))); + // print("Fecha de ingreso silicitada " # Int.toText(data.checkIn)); + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // if(now() + housing.minReservationLeadTimeNanoSec > data.checkIn){ + // return #Err("Reservations are requested at least " # + // Int.toText(housing.minReservationLeadTimeNanoSec /(NANO_SEG_PER_HOUR)) # + // " hours in advance."); + // }; + // if(availableAllDaysResquest(data.checkIn, data.checkOut)){ + // let reservationId = await ramdomGenerator.randRange(1_000_000_000, 9_999_999_999); + // let reservation = {data with applicant = caller}; + // let responseReservation = { + // housingId; + // reservationId; + // data = reservation; + // msg = msg.PayRequest; + // paymentCode = await ramdomGenerator.randRange(1_000_000_000_000_000_000_000, 9_999_999_999_999_999_999_999) + // }; + // ignore Map.put(housing.reservationRequests, nhash, reservationId, reservation ); + // #Ok( responseReservation ) + // } else { + // #Err( msg.NotAvalableAllDays); + // } + // }; + // } + // }; + // func paymentVerification(txHash: Nat):async Bool{ + // // TODO protocolo de verificacion de pago + // true + // }; + + // public shared ({ caller }) func confirmReservation({reservId: Nat; hostId: HousingId; txHash: Nat}): async {#Ok; #Err: Text}{ + // let housing = Map.get(housings, nhash, hostId); + // switch housing { + // case null { #Err(msg.NotHosting) }; + // case ( ?housing ) { + // let updatedCalendar = updateCalendar(housing.calendar); + // ignore Map.put(housings, nhash, hostId, {housing with updatedCalendar}); + // let reserv = Map.remove(housing.reservationRequests, nhash, reservId); + // switch reserv { + // case null { #Err(msg.NotReservation) }; + // case ( ?reserv ) { + // if(caller != reserv.applicant) { + // return #Err(msg.CallerIsNotrequester) + // }; + // // TODO Verificacion datos de pago a traves del txHhahs + // if (await paymentVerification(txHash)){ + // print("insertando la reserva en el calendario"); + // let calendar = insertReservationToCalendar(updatedCalendar, reserv); + // switch calendar { + // case (#Ok(calendar)) { + + // ignore Map.put(housings, nhash, hostId, {housing with calendar}); + // return #Ok + // }; + // case (_) { + // ignore Map.put(housing.reservationRequests, nhash, reservId, reserv); + // return #Err("Error") + // } + // } + // }; + // #Err("Incorrect payment verification") + // } + // } + // } + // } + // }; + + // // TODO confirmacion de reservacion por parte del dueño del Host + // public shared ({ caller }) func confirmReservation({reservId: Nat; hostId: HousingId}): async (){ + + // }; + + +}; + diff --git a/backend/types.mo b/backend/types.mo new file mode 100644 index 0000000..06bc1bd --- /dev/null +++ b/backend/types.mo @@ -0,0 +1,286 @@ +import Map "mo:map/Map"; +import Nat "mo:base/Nat"; +import List "mo:base/List"; + +module { + + ///////////////////////////////// Settings /////////////////////////////////// + + public type Settings = { + cancellationFeeCompensateBuyer: Nat64; + minDaysBeforeCheckinForCancellation: Nat; + timeToPay: Nat; + reservationFee: Nat64; + }; + + ///////////////////////////////// Users ///////////////////////////////////// + + public type SignUpData = { + firstName: Text; + lastName: Text; + phone: ?Nat; + email: Text; + referralBy: ?Nat32; + }; + + public type UserData = { + firstName: Text; + lastName: Text; + }; + + public type User = SignUpData and { + verified: Bool; + reservations: List.List; + score: Nat; + reviewsIssued: List.List; + }; + + public type HostUser = SignUpData and { + verified: Bool; + score: Nat; + housingIds: List.List; + housingTypes: Map.Map; + }; + + public type SignUpResult = { + #Ok: UserData; + #Err: Text; + }; + + public type Review = { + autor: Principal; + hostId: Nat; + date: Int; + body: Text; + }; + + ///////////////////////////////// Housing /////////////////////////////////// + + public type HousingCreateData = { + namePlace: Text; + nameHost: Text; + descriptionPlace: Text; + descriptionHost: Text; + link: Text; + photos: [Blob]; + thumbnail: Blob; + }; + + public type Housing = HousingCreateData and { + active: Bool; + housingId: Nat; + price: ?Price; + owner: Principal; + rules: [Rule]; + checkIn: Nat; + checkOut: Nat; + address: Location; + properties: ?HousingType; + housingType: ?Text; + amenities: ?Amenities; + encodedAmenities: Nat64; + encodedAmenities2: Nat64; // Prueba + reviews: List.List; + calendary: Calendary; + reservationsPending: [Nat]; + unavailability : {busy: [Int]; pending: [Int]} + }; + + public type HousingTypeInit = { + nameType: Text; + beds: [BedKind]; + bathroom: [Bathroom]; + maxGuest: Nat; + extraGuest: Nat; + }; + + public type HousingType = HousingTypeInit and { + housingIds: [HousingId]; + }; + + public type HousingId = Nat; + + public type HousingPreview = { + active: Bool; + housingId: Nat; + address: Location; + thumbnail: Blob; + price: ?Price; + encodedAmenities: Nat64; + }; + + public type HousingResponse = { + #Start: Housing and { + hasNextPhoto: Bool; + }; + #OnlyPhoto: { + photo: Blob; + hasNextPhoto: Bool; + }; + }; + + public type Bathroom = { + toilette: Bool; + shower: Bool; + bathtub: Bool; + isShared: Bool; + sink: Bool; + }; + + public type Rule = { + #PetsAllowed: Bool; + #SmookingAllowed: Bool; + #PartiesAllowed: Bool; + #AdditionalGuests: Bool; + #NoiseAfter10pm: Bool; + #ParkOnTheStreet: Bool; + #VisitsAllowed: Bool; + #CustomRule: {rule: Text; allowed: Bool}; + }; + + public type BedKind = { + #Single: Nat; + #Matrimonial: Nat; + #SofaBed: Nat; + }; + + public type Amenities = { + // + freeWifi: Bool; + airCond: Bool; // Aire acondicionado + flatTV: Bool; // TV de pantalla plana + minibar: Bool; + safeBox: Bool; // Caja de seguridad + roomService: Bool; // Servicio a la habitación + premiumLinen: Bool; // Ropa de cama premium + ironBoard: Bool; // Plancha y tabla de planchar + privateBath: Bool; // Baño privado con artículos de tocador + hairDryer: Bool; // Secador de pelo + hotelRest: Bool; // Restaurante en el hotel + barLounge: Bool; // Bar y lounge + buffetBrkfst: Bool; // Desayuno buffet + lobbyCoffee: Bool; // Servicio de café/té en el lobby + catering: Bool; // Servicio de catering para eventos + specialMenu: Bool; // Menú para dietas especiales (bajo solicitud) + outdoorPool: Bool; // Piscina al aire libre + spaWellness: Bool; // Spa y centro de bienestar + gym: Bool; + jacuzzi: Bool; + gameRoom: Bool; // Salón de juegos + tennisCourt: Bool; // Pista de tenis + natureTrails: Bool; // Acceso a senderos naturales + custom: [{amenitieName: Text; value: Bool}]; + }; + + public type Amenities2 = [Text]; // Eventualmente se pueden establecer las amenidades dinamicamente + public let amenitiesArray = ["freeWifi", "airCond", "flatTV", "minibar", "safeBox", "roomService", "premiumLinen", "ironBoard", "privateBath", "hairDryer", "hotelRest", "barLounge", + "buffetBrkfst", "lobbyCoffee", "catering", "specialMenu", "outdoorPool", "spaWellness", "gym", "jacuzzi", "gameRoom", "tennisCourt", "natureTrails"]; + + public type Location = { + country: Text; + city: Text; + neighborhood: Text; + zipCode: Nat; + street: Text; + externalNumber: Nat; + internalNumber: Nat; + geolocation: ?{lat: Int; lng: Int} + }; + + public type Price = { + base: Nat; + discountTable: [{minimumDays: Nat; discount: Nat}]; // Ej. [{minimumDays = 5; discount = 10}, {minimumDays = 15; discount = 15}] + }; + + ////////////////////////////// Referrals /////////////////////////////////// + + public type Refered = { + date: Int; + user: Principal; + kind: ReferalKind + }; + + public type ReferalKind = { + #User: StatusReferral; + #Host: StatusReferral + }; + + public type ReferralBook = { + owner: Principal; + refereds: [Refered]; + }; + public type StatusReferral = { + #Level1; + #Level2; + #Level3; + #Level4; + #Level5; + }; + + + ////////////////////////////// Reservations //////////////////////////////// + + public type Calendary = { + dayZero: Int; + reservations: [Reservation]; + }; + + public type ReservationDataInput = { + housingId: HousingId; + checkIn: Int; // Número de día de ingreso. Siendo 0 el día actual + checkOut: Int; // El egreso tiene que ser mayor que 1 + el ingreso + guest: Text; // Nombre del huésped + email: Text; + phone: Nat; + }; + + public type ReservationStatus = { + #Pending; + #Confirmed; + #Canceled; + #InProgress; + #Ended; + }; + + public type Reservation = ReservationDataInput and { + date: Int; + timestampCheckIn: Int; + // checkIn: Int; // Int es un supertipo de Nat por lo tanto no hay conflicto con el checkIn que se "hereda" de ReservationDataInput y queda como Int + // checkOut: Int; // Idem + reservationId: Nat; + requester: Principal; + ownerHousing: Principal; + // confirmated: Bool; + status: ReservationStatus; + amount: Nat64; + dataTransaction: DataTransaction; + }; + + public let NullTrx: DataTransaction = { + from = ""; + to = ""; + amount = 0 + }; + + public type TransactionParams = { + to: Text; + amount: Nat64; + }; + + public type DataTransaction = TransactionParams and { + from: Text; + }; + + public type TransactionResponse = { + #Ok: {transactionParams: TransactionParams; reservationId: Nat}; + #Err: Text; + }; + + ///////////////////////////////// General /////////////////////////////////// + + public type UpdateResult = { + #Ok; + #Err: Text; + }; + +} diff --git a/backend/typesOld.mo.txt b/backend/typesOld.mo.txt new file mode 100644 index 0000000..25763a7 --- /dev/null +++ b/backend/typesOld.mo.txt @@ -0,0 +1,233 @@ +import Map "mo:map/Map"; +import Nat "mo:base/Nat"; +module { + ////////////////////////////// Users ///////////////////////////////////// + + public type SignUpData = { + firstName: Text; + lastName: Text; + phone: ?Nat; + email: Text; + //avatar: ?Blob; + }; + + public type User = SignUpData and { + // id: Nat; + kinds: [UserKind]; + // userKind: UserKind; + verified: Bool; + score: Nat; + }; + + public type UserKind = { + #Initial; + #Guest: [ReviewsId]; + #Host: [HousingId]; + }; + + public type SignUpResult = { #Ok : User; #Err : Text }; + + // type GetProfileError = { + // #userNotAuthenticated; + // #profileNotFound; + // }; + + // type CreateProfileError = { + // #profileAlreadyExists; + // #userNotAuthenticated; + // }; + + ///////////////////////////////// Housing ///////////////////////////////// + + + public type HousingCreateData = { + namePlace: Text; + nameHost: Text; + descriptionPlace: Text; + descriptionHost: Text; + link: Text; + photos: [Blob]; + thumbnail: Blob; + + }; + + public type Rule = { + #PetsAllowed: Bool; + #SmookingAllowed: Bool; + #PartiesAllowed: Bool; + #AdditionalGuests: Bool; + #NoiseAfter10pm: Bool; + #ParkOnTheStreet: Bool; + #VisitsAllowed: Bool; + #CustomRule: {rule: Text; allowed: Bool}; + }; + + public type Amenities = { + freeWifi: Bool; + airCond: Bool; // Aire acondicionado + flatTV: Bool; // TV de pantalla plana + minibar: Bool; + safeBox: Bool; // Caja de seguridad + roomService: Bool; // Servicio a la habitación + premiumLinen: Bool; // Ropa de cama premium + ironBoard: Bool; // Plancha y tabla de planchar + privateBath: Bool; // Baño privado con artículos de tocador + hairDryer: Bool; // Secador de pelo + hotelRest: Bool; // Restaurante en el hotel + barLounge: Bool; // Bar y lounge + buffetBrkfst: Bool; // Desayuno buffet + lobbyCoffee: Bool; // Servicio de café/té en el lobby + catering: Bool; // Servicio de catering para eventos + specialMenu: Bool; // Menú para dietas especiales (bajo solicitud) + outdoorPool: Bool; // Piscina al aire libre + spaWellness: Bool; // Spa y centro de bienestar + gym: Bool; + jacuzzi: Bool; + gameRoom: Bool; // Salón de juegos + tennisCourt: Bool; // Pista de tenis + natureTrails: Bool; // Acceso a senderos naturales + custom: [{amenitieName: Text; value: Bool}]; + }; + + public type Location = { + country: Text; + city: Text; + neighborhood: Text; + zipCode: Nat; + street: Text; + externalNumber: Nat; + internalNumber: Nat; + }; + + public type Housing = HousingCreateData and { + active: Bool; + id: Nat; + price: ?Price; + owner: Principal; + rules: [Rule]; + checkIn: Int; + checkOut: Int; + address: Location; + properties: [Property]; + amenities: ?Amenities; + }; + + public type Bathroom = { + toilette: Bool; + shower: Bool; + bathtub: Bool; + isShared: Bool; + sink: Bool; + }; + public type Property = { + nameType: Text; + beds: [BedKind]; + bathroom: Bathroom; + maxGuest: Nat; + extraGuest: Nat; + }; + + // public type HousingDataInit = { + // minReservationLeadTimeNanoSec: Int; //valor en nanoSeg de anticipación para efectuar una reserva + // address: Text; + // prices: [Price]; + // kind: HousingKind; + // maxCapacity: Nat; + // description: Text; + // rules: [Text]; + // amenities: [Text]; + // properties: [Property]; + // }; + + // public type Housing = HousingDataInit and { + + // id: Nat; // Example L234324 + // owner: Principal; + // calendar: [var CalendaryPart]; + // reservationRequests: Map.Map; + // reviews: [Text]; + // photos: [Blob]; + // thumbnail: Blob; // Se recomienda la foto principal en tamaño reducido + // active: Bool; + // }; + + type BedKind = { + #Single: Nat; + #Matrimonial: Nat; + #SofaBed: Nat; + // Agregar mas variantes + }; + + public type HousingResponse = { + #Start : Housing and { + hasNextPhoto: Bool; + }; + #OnlyPhoto :{ + photo: Blob; + hasNextPhoto: Bool; + } + }; + + public type HousingPreview = { + active: Bool; + id: Nat; + address: Location; + thumbnail: Blob; + price: ?Price; + }; + + public type HousingId = Nat; + + public type UpdateResult = { + #Ok; + #Err: Text; + }; + + // public type HousingKind = { + // #House; + // #Hotel_room: Text; //Ejemplo #Hotel_room("Single Room") + // #RoomWithSharedSpaces: [Rule]; //Hostels/Pensiones + // }; + + // public type Price = { + // #PerNight: Nat; + // #PerWeek: Nat; + // #CustomPeriod: [{dais: Nat; price: Nat}]; + // }; + + public type Price = { + base: Nat; // price per nigth + discountTable: [{minimumDays: Nat; discount: Nat}]; // Ej. [{minimumDays = 5; discount = 10}, {minimumDays = 15; discount = 15}] + }; + + // public type Rules = { // Ejemplo de Rule: {key = "Horarios"; value = "Sin ruidos molestos entre las 22:00 y las 8:00"} + // key: Text; + // value: Text + // }; + + public type ReviewsId = Text; + ///////////////////////////////// Reservations ///////////////////////////// + + public type ReservationDataInput = { + checkIn: Int; //Timestamp NanoSeg + checkOut: Int; //Temestamp NanoSeg + guest: Text; + }; + + public type Reservation = ReservationDataInput and { + applicant: Principal; + }; + + // La primer posicion es siempre el dia actual con lo cual cada vez que se consulta se tiene que actualizar antes + // Para facilitar la implementacion inicial se considera una lista de Disponibility mutable de 30 posiciones + // public type Disponibility = {day: Int; available: Bool}; + + + // public type CalendaryPart = Disponibility and {reservation: ?Reservation}; + + + + +} + + \ No newline at end of file diff --git a/comandosCLI.md b/comandosCLI.md new file mode 100644 index 0000000..06ef1e0 --- /dev/null +++ b/comandosCLI.md @@ -0,0 +1,141 @@ +``` +dfx deploy backend + +``` +registro de usuario huesped general: + +``` +dfx canister call backend signUp '(record { + firstName="Juan"; + lastName="Perez"; + phone= null; + email="juanperez@gmail.com"; + } +)' +``` + +registro de usuario tipo Host + +``` +dfx canister call backend signUpAsHost '(record { + firstName="Gerardo"; + lastName="Anchorena"; + phone= opt 54221548797; + email="gerardonchorena@gmail.com"; + } +)' +``` + +publicacion de hosting + +``` +dfx canister call backend publishHousing '(record { + minReservationLeadTimeNanoSec = 86400000000000; + address = "San Martin 555"; + description= "Vista al mar, 56 Mts2, silencioso"; + maxCapacity= 6; + amenities = vec{"Jacuzzi"; "Piscina"; "Gimnasio"}; + rules = vec {"No fumar"}; + + prices = vec { + variant {PerNight = 90}; + variant {PerWeek = 550}; + variant {CustomPeriod = vec { + record {dais = 15; price = 1000}; + record {dais = 30; price = 1900}; + } + } + }; + kind = variant {House}; + properties = record{ + beds= vec{ + variant {Single = 2}; + variant {SofaBed = 2} + }; + bathroom = true + }; +} +)' +``` + +agregar foto a publicacion + +``` +dfx canister call backend addPhotoToHousing '(record { + id = 1; + photo = blob "00/11/22/33/44/"} +)' +``` + +agregar foto miniatura (foto principal de tamaño reducido) + +``` +dfx canister call backend addThumbnailToHousing '(record { + id = 1; + thumbnail = blob "00/66/88/44/45/98/45/98"} +)' +``` + +actualizacion de precios + +``` +dfx canister call backend updatePrices '(record { + id = 1; + prices = vec { + variant {PerNight = 400}; + variant {PerWeek = 2800}; + } +})' +``` +Set amenities + +``` +dfx canister call backend setAmenities '( record { + freeWifi = false; + airCond = false; + flatTV = false; + minibar = true; + safeBox = false; + roomService = false; + premiumLinen = false; + ironBoard = false; + privateBath = true; + hairDryer = false; + hotelRest = false; + barLounge = false; + buffetBrkfst = false; + lobbyCoffee = false; + catering = true; + specialMenu = false; + outdoorPool = false; + spaWellness = false; + gym = false; + jacuzzi = false; + gameRoom = true; + tennisCourt = false; + natureTrails = false; + custom = vec {}; +}, 1)' + +``` +Solicitud de reserva + +``` +dfx canister call backend requestReservation '(record { + housingId = 1; + checkIn = 9; + checkOut = 11; + guest = "Carlos" +})' + + +dfx canister call backend confirmReservation '(record { + reservationId = ; + txData = record { + to = ""; + amount = 4_000_000_000; + from = "" + } +})' + +``` diff --git a/dfx.json b/dfx.json index a02af53..b20cef8 100644 --- a/dfx.json +++ b/dfx.json @@ -2,6 +2,36 @@ "version": 1, "dfx": "0.20.1", "canisters": { + "backend": { + "main": "backend/main.mo", + "type": "motoko", + "declarations": { + "output": "frontend/src/declarations/backend" + } + + }, + + "tour": { + "type": "motoko", + "main": "tour/icrc1-custom.mo" + }, + + "icrc1_ledger_canister": { + "type": "custom", + "candid": "https://raw.githubusercontent.com/dfinity/ic/233c1ee2ef68c1c8800b8151b2b9f38e17b8440a/rs/ledger_suite/icrc1/ledger/ledger.did", + "wasm": "https://download.dfinity.systems/ic/233c1ee2ef68c1c8800b8151b2b9f38e17b8440a/canisters/ic-icrc1-ledger.wasm.gz" + }, + + "icrc1_index_canister": { + "type": "custom", + "candid": "https://raw.githubusercontent.com/dfinity/ic/a62848817cec7ae50618a87a526c85d020283fd9/rs/ledger_suite/icrc1/index-ng/index-ng.did", + "wasm": "https://download.dfinity.systems/ic/a62848817cec7ae50618a87a526c85d020283fd9/canisters/ic-icrc1-index-ng.wasm.gz" + }, + "icrc1_minter_canister": { + "type": "motoko", + "main": "tour/minter-canister.mo" + }, + "test": { "type": "motoko", "main": "backend/test/main.mo", @@ -11,11 +41,15 @@ } }, "frontend": { - "dependencies": ["test"], + "dependencies": [ + "test" + ], "frontend": { "entrypoint": "frontend/build/index.html" }, - "source": ["frontend/build"], + "source": [ + "frontend/build" + ], "type": "assets" }, "internet-identity": { @@ -29,5 +63,11 @@ } } } + }, + "output_env_file": ".env", + "defaults": { + "build": { + "packtool": "mops sources" + } } -} +} \ No newline at end of file diff --git a/interfaces/ic-management-interface.mo b/interfaces/ic-management-interface.mo new file mode 100644 index 0000000..e6b6ecf --- /dev/null +++ b/interfaces/ic-management-interface.mo @@ -0,0 +1,77 @@ +import Prim "mo:⛔"; + +module { + public type canister_id = Principal; + + public type canister_settings = { + freezing_threshold : ?Nat; + controllers : ?[Principal]; + memory_allocation : ?Nat; + compute_allocation : ?Nat; + }; + + public type definite_canister_settings = { + freezing_threshold : Nat; + controllers : [Principal]; + memory_allocation : Nat; + compute_allocation : Nat; + }; + + public type wasm_module = [Nat8]; + + public type Self = actor { + canister_status : shared { canister_id : canister_id } -> async { + status : { #stopped; #stopping; #running }; + memory_size : Nat; + cycles : Nat; + settings : definite_canister_settings; + idle_cycles_burned_per_day : Nat; + module_hash : ?[Nat8]; + }; + + create_canister : shared { settings : ?canister_settings } -> async { + canister_id : canister_id; + }; + + delete_canister : shared { canister_id : canister_id } -> async (); + + deposit_cycles : shared { canister_id : canister_id } -> async (); + + install_code : shared { + arg : [Nat8]; + wasm_module : wasm_module; + mode : { #reinstall; #upgrade; #install }; + canister_id : canister_id; + } -> async (); + + start_canister : shared { canister_id : canister_id } -> async (); + + stop_canister : shared { canister_id : canister_id } -> async (); + + uninstall_code : shared { canister_id : canister_id } -> async (); + + update_settings : shared { + canister_id : Principal; + settings : canister_settings; + } -> async (); + + }; + + public func addController(canister_id : Principal, controller : Principal) : async () { + let self = actor("aaaaa-aa"): Self; + let currentSetings = (await self.canister_status({canister_id})).settings; + let controllers = Prim.Array_tabulate ( + currentSetings.controllers.size() + 1, + func i = if(i == 0) {controller} else {currentSetings.controllers[i - 1]} + ); + await self.update_settings({ + canister_id; + settings = { + freezing_threshold = ?currentSetings.freezing_threshold; + controllers = ?controllers; + memory_allocation = ?currentSetings.memory_allocation; + compute_allocation = ?currentSetings.compute_allocation + }; + }); + }; +}; diff --git a/mops.toml b/mops.toml new file mode 100644 index 0000000..c64c82e --- /dev/null +++ b/mops.toml @@ -0,0 +1,5 @@ +[dependencies] +base = "0.11.1" +map = "9.0.1" +account-identifier = "1.0.2" +icrc2-mo = "0.0.13" \ No newline at end of file diff --git a/package.json b/package.json index 8f8f1bc..b8aec4c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ "typescript" ], "scripts": { + "deploy-with-content": "chmod +x scripts-sh/deploy-with-content.sh; ./scripts-sh/deploy-with-content.sh", + "secuencial-deploy": "chmod +x scripts-sh/deploy-general.sh; ./scripts-sh/deploy-general.sh", + "deploy-token": "chmod +x scripts-sh/deploy-token.sh; ./scripts-sh/deploy-token.sh", + "test-token": "chmod +x scripts-sh/test-token.sh; ./scripts-sh/test-token.sh", "build": "turbo run build", "preclean": "turbo run clean", "clean": "rm -rf .dfx && rm -rf .turbo && rm -rf node_modules & rm -rf src/declarations" diff --git a/scripts-sh/deploy-general.sh b/scripts-sh/deploy-general.sh new file mode 100755 index 0000000..0af8c1a --- /dev/null +++ b/scripts-sh/deploy-general.sh @@ -0,0 +1,27 @@ + +dfx identity new triourism +dfx identity use triourism + +# Deploy del canister principal +dfx deploy backend +export backend=$(dfx canister id backend) + +# Deploy del Minter Canister +dfx deploy icrc1_minter_canister --argument '( + record {triourismCanisterId = principal "'$backend'"} +)' +export minterCanister=$(dfx canister id icrc1_minter_canister) + +# Referencia al Minter en el backend +echo "Seteando referencia al canister minter en el canister backend..." +dfx canister call backend setMinter '(principal "'$minterCanister'")' + +echo -e "\n\nSiguientes pasos: + 1: Deploy del canister Ledger + 2: Referenciar el ledger en el canister minter + dfx canister call icrc1_minter_canister setLedger '(principal "'$(dfx canister id tour)'")' + 3: Referenciar el canister Minteren el canister principal + dfx canister call backend setMinter '(principal "'$(dfx canister id icrc1_minter_canister)'")' +" + +# diff --git a/scripts-sh/deploy-token.sh b/scripts-sh/deploy-token.sh new file mode 100755 index 0000000..2f354e5 --- /dev/null +++ b/scripts-sh/deploy-token.sh @@ -0,0 +1,226 @@ + +# dfx deploy icrc1_ledger_canister --argument '( variant { +# Init = record { +# decimals = opt (8 : nat8); +# token_symbol = "TOUR"; +# transfer_fee = 10_000 : nat; +# metadata = vec { +# record { +# "icrc1:logo"; +# variant {Text = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCI+CiAgICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iYmx1ZSI+CiAgICAgICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iciIgZnJvbT0iNDAiIHRvPSIyMCIgZHVyPSIxcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+CiAgICA8L2NpcmNsZT4KPC9zdmc+Cgo=" +# } +# }; +# record { "icrc1:decimals"; variant { Nat = 8 : nat } }; +# record { "icrc1:name"; variant { Text = "$TOUR" } }; +# record { "icrc1:symbol"; variant { Text = "TOUR" } }; +# record { "icrc1:fee"; variant { Nat = 10_000 : nat } }; +# record { "icrc1:max_memo_length"; variant { Nat = 80 : nat } }; +# }; +# minting_account = record { +# owner = principal "y77j5-4vnxl-ywos7-qjtcr-6iopc-i2ql2-iwoem-ehvwk-wruju-fr7ib-mae"; +# subaccount = null; +# }; +# initial_balances = vec { +# record { +# record { +# owner = principal "zpdk5-e6ec5-izoeb-uzhwy-rl2ot-4ag42-im6yv-itg3x-inywa-j3bae-tqe"; +# subaccount = null; +# }; +# 1_000_000_000_000_000 : nat; +# }; +# record { +# record { +# owner = principal "xigzi-mf2wo-xch5n-4dlsf-5tq6n-pke7b-7w2tx-2fv4h-l3yvi-3ycr2-pae"; +# subaccount = null; +# }; +# 500_000_000_000_000: nat; +# } +# }; +# fee_collector_account = opt record { +# owner = principal "epvyw-ddnza-4wy4p-joxft-ciutt-s7pji-cfxm3-khwlb-x2tb7-uo7tc-xae"; +# subaccount = null; +# }; +# archive_options = record { +# num_blocks_to_archive = 1_000 : nat64; +# max_transactions_per_response = null; +# trigger_threshold = 2_000 : nat64; +# more_controller_ids = opt vec { +# principal "d2alm-ajpbz-hohks-j3k3y-ulxfm-fegz6-jwopx-d2eu7-3ycil-hnxqa-hae"; +# }; +# max_message_size_bytes = null; +# cycles_for_archive_creation = opt (10_000_000_000_000 : nat64); +# node_max_memory_size_bytes = null; +# controller_id = principal "epvyw-ddnza-4wy4p-joxft-ciutt-s7pji-cfxm3-khwlb-x2tb7-uo7tc-xae"; +# }; +# max_memo_length = null; +# token_name = "$TOUR"; +# feature_flags = opt record { icrc2 = true }; +# } +# } +# )' + +# dfx deploy icrc1_index_canister --argument '(opt variant { +# Init = record { +# ledger_id = principal "br5f7-7uaaa-aaaaa-qaaca-cai"; +# retrieve_blocks_from_ledger_interval_seconds = opt 30 +# } +# })' + + +######################################################################## +# Ejemplo de metadata de ckBTC +#( +# vec { +# record { +# "icrc1:logo"; +# variant { +# Text = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQ2IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDE0NiAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDYiIGhlaWdodD0iMTQ2IiByeD0iNzMiIGZpbGw9IiMzQjAwQjkiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNi4zODM3IDc3LjIwNTJDMTguNDM0IDEwNS4yMDYgNDAuNzk0IDEyNy41NjYgNjguNzk0OSAxMjkuNjE2VjEzNS45MzlDMzcuMzA4NyAxMzMuODY3IDEyLjEzMyAxMDguNjkxIDEwLjA2MDUgNzcuMjA1MkgxNi4zODM3WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzExMF81NzIpIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjguNzY0NiAxNi4zNTM0QzQwLjc2MzggMTguNDAzNiAxOC40MDM3IDQwLjc2MzcgMTYuMzUzNSA2OC43NjQ2TDEwLjAzMDMgNjguNzY0NkMxMi4xMDI3IDM3LjI3ODQgMzcuMjc4NSAxMi4xMDI2IDY4Ljc2NDYgMTAuMDMwMkw2OC43NjQ2IDE2LjM1MzRaIiBmaWxsPSIjMjlBQkUyIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTI5LjYxNiA2OC43MzQzQzEyNy41NjYgNDAuNzMzNSAxMDUuMjA2IDE4LjM3MzQgNzcuMjA1MSAxNi4zMjMyTDc3LjIwNTEgMTBDMTA4LjY5MSAxMi4wNzI0IDEzMy44NjcgMzcuMjQ4MiAxMzUuOTM5IDY4LjczNDNMMTI5LjYxNiA2OC43MzQzWiIgZmlsbD0idXJsKCNwYWludDFfbGluZWFyXzExMF81NzIpIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNzcuMjM1NCAxMjkuNTg2QzEwNS4yMzYgMTI3LjUzNiAxMjcuNTk2IDEwNS4xNzYgMTI5LjY0NyA3Ny4xNzQ5TDEzNS45NyA3Ny4xNzQ5QzEzMy44OTcgMTA4LjY2MSAxMDguNzIyIDEzMy44MzcgNzcuMjM1NCAxMzUuOTA5TDc3LjIzNTQgMTI5LjU4NloiIGZpbGw9IiMyOUFCRTIiLz4KPHBhdGggZD0iTTk5LjgyMTcgNjQuNzI0NUMxMDEuMDE0IDU2Ljc1MzggOTQuOTQ0NyA1Mi40Njg5IDg2LjY0NTUgNDkuNjEwNEw4OS4zMzc2IDM4LjgxM0w4Mi43NjQ1IDM3LjE3NUw4MC4xNDM1IDQ3LjY4NzlDNzguNDE1NSA0Ny4yNTczIDc2LjY0MDYgNDYuODUxMSA3NC44NzcxIDQ2LjQ0ODdMNzcuNTE2OCAzNS44NjY1TDcwLjk0NzQgMzQuMjI4NUw2OC4yNTM0IDQ1LjAyMjJDNjYuODIzIDQ0LjY5NjUgNjUuNDE4OSA0NC4zNzQ2IDY0LjA1NiA0NC4wMzU3TDY0LjA2MzUgNDQuMDAyTDU0Ljk5ODUgNDEuNzM4OEw1My4yNDk5IDQ4Ljc1ODZDNTMuMjQ5OSA0OC43NTg2IDU4LjEyNjkgNDkuODc2MiA1OC4wMjM5IDQ5Ljk0NTRDNjAuNjg2MSA1MC42MSA2MS4xNjcyIDUyLjM3MTUgNjEuMDg2NyA1My43NjhDNTguNjI3IDYzLjYzNDUgNTYuMTcyMSA3My40Nzg4IDUzLjcxMDQgODMuMzQ2N0M1My4zODQ3IDg0LjE1NTQgNTIuNTU5MSA4NS4zNjg0IDUwLjY5ODIgODQuOTA3OUM1MC43NjM3IDg1LjAwMzQgNDUuOTIwNCA4My43MTU1IDQ1LjkyMDQgODMuNzE1NUw0Mi42NTcyIDkxLjIzODlMNTEuMjExMSA5My4zNzFDNTIuODAyNSA5My43Njk3IDU0LjM2MTkgOTQuMTg3MiA1NS44OTcxIDk0LjU4MDNMNTMuMTc2OSAxMDUuNTAxTDU5Ljc0MjYgMTA3LjEzOUw2Mi40MzY2IDk2LjMzNDNDNjQuMjMwMSA5Ni44MjEgNjUuOTcxMiA5Ny4yNzAzIDY3LjY3NDkgOTcuNjkzNEw2NC45OTAyIDEwOC40NDhMNzEuNTYzNCAxMTAuMDg2TDc0LjI4MzYgOTkuMTg1M0M4NS40OTIyIDEwMS4zMDYgOTMuOTIwNyAxMDAuNDUxIDk3LjQ2ODQgOTAuMzE0MUMxMDAuMzI3IDgyLjE1MjQgOTcuMzI2MSA3Ny40NDQ1IDkxLjQyODggNzQuMzc0NUM5NS43MjM2IDczLjM4NDIgOTguOTU4NiA3MC41NTk0IDk5LjgyMTcgNjQuNzI0NVpNODQuODAzMiA4NS43ODIxQzgyLjc3MiA5My45NDM4IDY5LjAyODQgODkuNTMxNiA2NC41NzI3IDg4LjQyNTNMNjguMTgyMiA3My45NTdDNzIuNjM4IDc1LjA2ODkgODYuOTI2MyA3Ny4yNzA0IDg0LjgwMzIgODUuNzgyMVpNODYuODM2NCA2NC42MDY2Qzg0Ljk4MyA3Mi4wMzA3IDczLjU0NDEgNjguMjU4OCA2OS44MzM1IDY3LjMzNEw3My4xMDYgNTQuMjExN0M3Ni44MTY2IDU1LjEzNjQgODguNzY2NiA1Ni44NjIzIDg2LjgzNjQgNjQuNjA2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMTEwXzU3MiIgeDE9IjUzLjQ3MzYiIHkxPSIxMjIuNzkiIHgyPSIxNC4wMzYyIiB5Mj0iODkuNTc4NiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjEiIHN0b3AtY29sb3I9IiNFRDFFNzkiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTIyNzg1Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl8xMTBfNTcyIiB4MT0iMTIwLjY1IiB5MT0iNTUuNjAyMSIgeDI9IjgxLjIxMyIgeTI9IjIyLjM5MTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjIxIiBzdG9wLWNvbG9yPSIjRjE1QTI0Ii8+CjxzdG9wIG9mZnNldD0iMC42ODQxIiBzdG9wLWNvbG9yPSIjRkJCMDNCIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==" +# }; +# }; +# record { "icrc1:decimals"; variant { Nat = 8 : nat } }; +# record { "icrc1:name"; variant { Text = "ckBTC" } }; +# record { "icrc1:symbol"; variant { Text = "ckBTC" } }; +# record { "icrc1:fee"; variant { Nat = 10 : nat } }; +# record { "icrc1:max_memo_length"; variant { Nat = 80 : nat } }; +# }, +# ) +###################################################################### +dfx identity new 0000InvNonVesting +dfx identity use 0000InvNonVesting +export InvNonVesting=$(dfx identity get-principal) + +dfx identity new 0000InvVesting +dfx identity use 0000InvVesting +export InvVesting=$(dfx identity get-principal) + +dfx identity new 0000Founder01 +dfx identity use 0000Founder01 +export Founder01=$(dfx identity get-principal) + +dfx identity new 0000Founder02 +dfx identity use 0000Founder02 +export Founder02=$(dfx identity get-principal) + +dfx identity new 0000Founder03 +dfx identity use 0000Founder03 +export Founder03=$(dfx identity get-principal) + + +dfx identity new 0000Controller +dfx identity use 0000Controller +export Controller=$(dfx identity get-principal) + +dfx identity use triourism +export deployer=$(dfx identity get-principal) +export minterCanister=$(dfx canister id icrc1_minter_canister) + +timestamp=$(date +%s) +cliffFounders=$((timestamp + 83)) +cliffInvestors=$((timestamp + 120)) + +dfx deploy tour --argument '( + variant { + Init = record { + decimals = 8 : nat8; + token_symbol = "TOUR"; + transfer_fee = 10_000 : nat; + metadata = vec { + record { + "icrc1:logo"; + variant { + Text = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCI+CiAgICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iYmx1ZSI+CiAgICAgICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iciIgZnJvbT0iNDAiIHRvPSIyMCIgZHVyPSIxcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+CiAgICA8L2NpcmNsZT4KPC9zdmc+Cgo=" + }; + }; + record { "icrc1:decimals"; variant { Nat = 8 : nat } }; + record { "icrc1:name"; variant { Text = "TOUR" } }; + record { "icrc1:symbol"; variant { Text = "$TOUR" } }; + record { "icrc1:fee"; variant { Nat = 10_000 : nat } }; + record { "icrc1:max_memo_length"; variant { Nat = 32 : nat } }; + }; + minting_account = record { + owner = principal "'$minterCanister'"; + subaccount = null; + }; + initial_balances = vec {}; + fee_collector_account = opt record { + owner = principal "'$minterCanister'"; + subaccount = opt blob "FeeCollector00000000000000000000"; + }; + archive_options = record { + num_blocks_to_archive = 1_000 : nat64; + max_transactions_per_response = null; + trigger_threshold = 2_000 : nat64; + more_controller_ids = null; + max_message_size_bytes = null; + cycles_for_archive_creation = opt (10_000_000_000_000 : nat64); + node_max_memory_size_bytes = null; + controller_id = principal "'$Controller'"; + }; + max_supply = null; + max_memo_length = opt (32 : nat); + token_name = "$TOUR"; + feature_flags = opt record { icrc2 = true }; + } + }, + record { + metadata = vec {}; + min_burn_amount = null; + max_supply = null; + distribution = opt record { + allocations = vec { + record { + categoryName = "Founders"; + holders = vec { + record { + owner = principal "'$Founder01'"; + hasVesting = true; + allocatedAmount = 20_000_000_000 : nat; + }; + record { + owner = principal "'$Founder02'"; + hasVesting = true; + allocatedAmount = 20_000_000_000 : nat; + }; + record { + owner = principal "'$Founder03'"; + hasVesting = true; + allocatedAmount = 30_000_000_000 : nat; + }; + }; + vestingScheme = variant { + timeBasedVesting = record { + cliff = opt ('"$cliffFounders"'); + intervalDuration = 23; + intervalQty = 12 : nat8; + } + } + }; + record { + categoryName = "Investors"; + holders = vec { + record { + owner = principal "'$InvVesting'"; + hasVesting = true; + allocatedAmount = 450_000_000_000 : nat; + }; + record { + owner = principal "'$InvNonVesting'"; + hasVesting = false; + allocatedAmount = 480_000_000_000 : nat; + }; + + }; + vestingScheme = variant { + timeBasedVesting = record { + cliff = opt ('"$cliffInvestors"'); + intervalDuration = 27; + intervalQty = 6 : nat8; + } + } + }; + }; + }; + }, +)' + +dfx canister call tour initialize diff --git a/scripts-sh/deploy-with-content.sh b/scripts-sh/deploy-with-content.sh new file mode 100755 index 0000000..9aaeb9d --- /dev/null +++ b/scripts-sh/deploy-with-content.sh @@ -0,0 +1,247 @@ +# #!/bin/bash +# # ------------ Usuario deployer ------------ +# dfx identity new 0000TestUser0 +# dfx identity use 0000TestUser0 +# dfx deploy backend + +# ------------ Usuario Host 1 -------------- +dfx identity new 0000TestUser1 +dfx identity use 0000TestUser1 +echo "registro de User Host Alberto Campos ... " +dfx canister call backend signUpAsHost '(record { + firstName="Alberto"; + lastName="Campos"; + email="null"; + phone = opt 542238780892; + referralBy = null; +})' +# CreateHousing 1 +echo "Alberto registra un housing ... " +dfx canister call backend createHousing '(record { + namePlace="Far West"; + nameHost="Alberto Campos"; + descriptionPlace="Lugar tranquilo y seguro"; + descriptionHost="Al lado de Far West disco bar y apuestas"; + link="https://media.istockphoto.com/id/2045383950/photo/digital-render-of-a-serene-bedroom-oasis-with-natural-light.jpg?s=2048x2048&w=is&k=20&c=2Rw4fqnC08kfiuc58jhzbCAOYwPp9V8w4e3Ma2TY984="; + photos = vec {}; + thumbnail = blob "Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABAAEAAAA" +})' +# Update Prices housing 1 +echo "Alberto setea el precio del housing de id 1, o sea el que acaba de crear ... " +dfx canister call backend updatePrices '(record { + id = 1 : nat; + price = record { + base = 100_000_000 : nat; + discountTable = vec { + record { minimumDays = 5 : nat; discount = 5 : nat }; + record { minimumDays = 10 : nat; discount = 15 : nat }; + }; + } +})' + +dfx canister call backend setAmenities '( record { + freeWifi = false; + airCond = false; + flatTV = false; + minibar = true; + safeBox = false; + roomService = false; + premiumLinen = false; + ironBoard = false; + privateBath = false; + hairDryer = true; + hotelRest = false; + barLounge = false; + buffetBrkfst = false; + lobbyCoffee = false; + catering = false; + specialMenu = false; + outdoorPool = false; + spaWellness = false; + gym = false; + jacuzzi = true; + gameRoom = false; + tennisCourt = false; + natureTrails = false; + custom = vec {}; +}, 1)' + + + + +#Assing housing Type +echo "Alberto crea un tipo de habitacion a partir de su housing de id 1 indicando que existen 4 ... " +dfx canister call backend cloneHousingWithProperties '(record { + housingId = 1 : nat; + qty = 1 : nat; + housingTypeInit = record { + extraGuest = 2 : nat; + bathroom = vec {record { + shower = true; + sink = true; + toilette = true; + isShared = false; + bathtub = true; + }}; + beds = vec { variant { Matrimonial = 1 : nat } }; + maxGuest = 4 : nat; + nameType = "Standard"; + }; +})' + +#Set Address Housing 1 +echo "Alberto establece la dirección de su housing de id 1" + +dfx canister call backend setAddress '(record { + housingId = 1 : nat; + address = record { + street = "9 de Julio"; + externalNumber = 1207; + internalNumber = 43; + city = "Mar del Plata"; + neighborhood = "Centro"; + country = "Argentina"; + zipCode = 7600 ; + }; +})' + +echo "Alberto publica su housing de id 1 ..." +dfx canister call backend publishHousing 1 + +# CreateHousing 2 +echo "Alberto crear otro housing ..." +dfx canister call backend createHousing '(record { + namePlace=""; + nameHost="Alberto Campos"; + descriptionPlace="Lugar tranquilo y seguro"; + descriptionHost="Espacioso y luminoso con vista al mar"; + link="https://media.istockphoto.com/id/1837566278/photo/scandinavian-style-apartment-interior.jpg?s=2048x2048&w=is&k=20&c=ZT-ZoefdikBU9DhdEg4fV6bW-SdZi_HLRFg_mupNd9E="; + photos = vec {}; + thumbnail = blob "Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABAAEAALKLKAKHUJHAA" +})' + +echo codigo de referidos de alberto... +export albertoCode=$(dfx canister call backend getMyReferralCode) +echo $albertoCode + + + +# ------------ Usuario Host 2 -------------- +dfx identity new 0000TestUser2 +dfx identity use 0000TestUser2 +echo "se registra una usuario llamada Lucila..." +dfx canister call backend signUpAsUser '(record { + firstName="Lucila"; + lastName="Peralta"; + email="lucilaperalta@live.com"; + phone = opt 542298712438 +})' + +# ------------ Usuario Host 3 -------------- +echo "se registra una UserHost llamada Claudia..." +dfx identity new 0000TestUser3 +dfx identity use 0000TestUser3 +dfx canister call backend signUpAsHost '(record { + firstName="Claudia"; + lastName="Gimenez"; + email="claugimenez@gmail.com"; + phone = opt 558789878522; +})' + +# ------------ Usuario Host 4 -------------- +dfx identity new 0000TestUser4 +dfx identity use 0000TestUser4 +echo "se registra un usuario llamado Mario..." +dfx canister call backend signUpAsUser '(record { + firstName="Mario"; + lastName="Pappa"; + email="mariopapa@gmail.com"; + phone = opt 542235227692; +})' + +# ------------ Usuario Host 5 -------------- +dfx identity new 0000TestUser5 +dfx identity use 0000TestUser5 +echo "se registra un UserHost llamado Rodolfo..." +dfx canister call backend signUpAsHost '(record { + firstName="Rodolfo"; + lastName="Anchorena"; + email="rodolfoanchorena.com"; + phone = opt 542278795421 +})' + +# ------------ Usuario Host 6 -------------- +dfx identity new 0000TestUser6 +dfx identity use 0000TestUser6 +dfx canister call backend signUpAsUser '(record { + firstName="Carlos"; + lastName="Maldonado"; + email="carlosmaldonado.com"; + phone = opt 5422145789544 +})' + +# ------------ Usuario 4 solicita una reserva de 6 dias para dentro de 3 dias +dfx identity use 0000TestUser4 +dfx canister call backend requestReservation '(record { + housingId = 1; + checkIn = 7; + checkOut = 10; + guest = "Mario"; + email = "mario@gmil.com"; + phone = 542236676567 +})' + +# ------------ Usuario 4 confirma la reserva + +dfx canister call backend confirmReservation '(record { + reservationId = 1; + txData = record { + to = "walletHousingInRequest"; + amount = 4_000_000_000; + from = "walletUser" + } +})' +# ------------ Usuario 2 pide reserva para dentro de 12 dias y se queda 3 + +dfx identity use 0000TestUser2 +dfx canister call backend requestReservation '(record { + housingId = 1; + checkIn = 12; + checkOut = 14; + guest = "Lucila"; + email = "cucila@gmil.com"; + phone = 556578787998 +})' + +# ------------ Usuario 2 confirma la reserva + +dfx canister call backend confirmReservation '(record { + reservationId = 2; + txData = record { + to = "walletHousingInRequest"; + amount = 4_000_000_000; + from = "walletUser" + } +})' +# ------------ Usuario 6 pide reserva para dentro de 9 dias y se queda 2 + +dfx identity use 0000TestUser6 +dfx canister call backend requestReservation '(record { + housingId = 1; + checkIn = 21; + checkOut = 31; + guest = "Carlos"; + email = "carlos@gmil.com"; + phone = 536657090 +})' + + +# ------------ Usuario 6 confirma la reserva +dfx canister call backend confirmReservation '(record { + reservationId = 4; + txData = record { + to = "walletHousingInRequest"; + amount = 4_000_000_000; + from = "walletUser" + } +})' \ No newline at end of file diff --git a/scripts-sh/test-token.sh b/scripts-sh/test-token.sh new file mode 100755 index 0000000..2eb3f09 --- /dev/null +++ b/scripts-sh/test-token.sh @@ -0,0 +1,411 @@ +#!/bin/bash + +run_test() { + DESCRIPTION="$1" + EXPECTED="$2" + COMMAND="$3" + + echo "-------------- $DESCRIPTION --------------" + echo "Valor esperado: $EXPECTED" + + # Ejecutar el comando y capturar la salida + RESULT=$(eval "$COMMAND") + + echo "Valor obtenido: $RESULT" + + # Verificar si el resultado contiene el valor esperado + if echo "$RESULT" | grep -q "$EXPECTED"; then + echo -e "\e[32m✔ Test exitoso\e[0m" # Verde + else + echo -e "\e[31m✘ Test fallido\e[0m" # Rojo + fi + echo "" +} + +# Configuración de identidades +dfx identity use 0000InvNonVesting +export InvNonVesting=$(dfx identity get-principal) + +dfx identity use 0000InvVesting +export InvVesting=$(dfx identity get-principal) + +dfx identity use 0000Founder01 +export Founder01=$(dfx identity get-principal) + +dfx identity use 0000Founder02 +export Founder02=$(dfx identity get-principal) + +dfx identity use 0000Founder03 +export Founder03=$(dfx identity get-principal) + +dfx identity use 0000Minter +export Minter=$(dfx identity get-principal) + +dfx identity use 0000FeeCollector +export FeeCollector=$(dfx identity get-principal) + +dfx identity use 0000Controller +export Controller=$(dfx identity get-principal) + +# Test balance luego de distribución + +echo -e "\n\n===== PRUEBAS PREVIAS AL INICIO DE VESTING distribucion con vesting bloqueada =====\n" + +run_test "Test total_supply luego de distribución" \ + "(1_000_000_000_000 : nat)" \ + "dfx canister call tour icrc1_total_supply" + +# Test usuario con vesting intentando transferir tokens +dfx identity use 0000InvVesting +run_test "Test usuario con vesting quiere transferir 500_000_000 tokens" \ + "( + variant { + Err = variant { + VestingRestriction = record { + blocked_amount = 450_000_000_000 : nat; + available_amount = 0 : nat; + } + } + }, + )" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder01\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 500_000_000 : nat; + }, + )'" + +# Test usuario sin vesting realizando transferencia +dfx identity use 0000InvNonVesting +run_test "Test usuario SIN VESTING puede transferir 500_000_000 tokens a founder1" \ + "(variant { Ok = 5 : nat })" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder01\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 500_000_000 : nat; + }, + )'" + +run_test "Verificación de balance de usuario sin vesting luego de la transferencia" \ + "(479_499_990_000 : nat)" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$InvNonVesting\" }, + )'" + +run_test "Verificación de balance de founder1 luego de la transferencia" \ + "(20_500_000_000 : nat)" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$Founder01\" }, + )'" + +run_test "Verificacion de balance del fee_collector luego de una transaccion" \ + "(10_000 : nat)" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$Minter\"; subaccount = opt blob \"FeeCollector00000000000000000000\"}, + )'" + +# Test mint y verificación de balance +dfx identity use 0000Minter +run_test "El minter hace un mint de 2_000_000_000 tokens en favor de founder1" \ + "(variant { Ok = 6 : nat })" \ + "dfx canister call tour mint '( + record { + to = record { owner = principal \"$Founder01\"; subaccount = null; }; + memo = null; + created_at_time = null; + amount = 2_000_000_000 : nat; + }, + )'" + +run_test "Verificación de balance de founder1" \ + "(22_500_000_000 : nat)" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$Founder01\" }, + )'" + +run_test "Check total_supply luego del mint" \ + "(1_002_000_000_000 : nat)" \ + "dfx canister call tour icrc1_total_supply" + + +# Founder 1 intenta transferir más de lo permitido +dfx identity use 0000Founder01 +run_test "Founder 1 quiere transferir 3_000_000_000 a founder 2 (bloqueado por vesting)" \ + "( + variant { + Err = variant { + VestingRestriction = record { + blocked_amount = 20_000_000_000 : nat; + available_amount = 2_500_000_000 : nat; + } + } + }, + )" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder02\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 3_000_000_000 : nat; + }, + )'" + +run_test "Founder 1 quiere transferir 2_500_000_000 a founder 2 (bloqueado por vesting)" \ + "( + variant { + Err = variant { + VestingRestriction = record { + blocked_amount = 20_000_000_000 : nat; + available_amount = 2_500_000_000 : nat; + } + } + }, + )" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder02\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 2_500_000_000 : nat; + }, + )'" + +run_test "Founder 1 quiere transferir 2_499_990_000 a founder 2 (exitosa)" \ + "(variant { Ok = 7 : nat })" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder02\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 2_499_990_000 : nat; + }, + )'" + + +echo -e "\n\n============= PRUEBAS DE VESTING PROGRESIVO =============\n" + +echo -e "\n============= Verificación del tiempo restante para el siguiente release =============\n" +now=$(date +%s) + +#_____________ +mapfile -t next_release_entries < <(dfx canister call tour vestingsStatus | grep -E 'categoryName =|nextReleaseTime =' | awk -F '= ' '{print $2}' | sed 's/;//g' | tr -d '"') + +min=5000000000000000000 +nextCategory="" +now=$(date +%s) +currentCategory="" + +# Iterar sobre las entradas extraídas +for ((i=0; i<${#next_release_entries[@]}; i++)); do + entry="${next_release_entries[i]}" + + # Si la entrada es un categoryName, guardarlo temporalmente + if [[ "$entry" =~ ^[A-Za-z]+$ ]]; then + currentCategory="$entry" + + # Si la entrada es un timestamp, procesarlo + elif [[ "$entry" != "null" ]]; then + num=$(echo "$entry" | grep -oP '\d+(_\d+)*' | tr -d '_') + + if [[ -n "$num" && "$num" -lt "$min" ]]; then + min="$num" + nextCategory="$currentCategory" + fi + fi +done + +# Convertir timestamp a segundos +min_seconds=$((min / 1000000000)) +secondsWaiting=$((min_seconds - now)) + +# Imprimir resultados +echo "La proxima liberación de fondos corresponde a la categoria: $nextCategory" +echo "Segundos restantes: $secondsWaiting" + + + +# Función para esperar mostrando cuenta regresiva +wait_with_countdown() { + local seconds=$1 + echo -e "\n[⏳] Esperando $seconds segundos..." + while [ $seconds -gt -1 ]; do + echo -ne " Tiempo restante: $seconds segundos\r" + sleep 1 + ((seconds--)) + done + echo -e "\n[✅] Continuando con las pruebas\n" +} + +wait_with_countdown $((secondsWaiting)) + +echo -e "\n\n============= PRUEBAS DE TRANSACCIONES EN CASOS LÍMITE PARA FOUNDERS =============\n" + +# Verificar si la próxima categoría es Founders +if [[ "$nextCategory" == "Founders" ]]; then + echo "La próxima liberación de fondos es para la categoría Founders. Procediendo con las pruebas..." + + # Seleccionar una identidad Founder + dfx identity use 0000Founder03 + + run_test "Founder03 intenta transferir el monto exacto disponible" \ + "( + variant { + Err = variant { + VestingRestriction = record { + blocked_amount = 27_500_000_000 : nat; + available_amount = 2_500_000_000 : nat; + } + } + }, + )" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder02\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 2_500_000_000 : nat; + }, + )'" + + # Intentar transferir justo el valor disponible menos la comisión + run_test "Founder03 intenta transferir el valor disponible exacto menos la comisión" \ + "(variant { Ok = 8 : nat })" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder02\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 2_490_990_000 : nat; + }, + )'" + + + run_test "Verificación de balance de founder1 luego de la transferencia" \ + "(20_000_000_000 : nat)" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$Founder01\" }, + )'" + + # Verificar el balance del receptor (Founder02) después de la transferencia + run_test "Verificación de balance de Founder02 después de la transferencia" \ + "(24_990_980_000 : nat" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$Founder02\" }, + )'" + + # Verificar el balance del fee_collector después de la transferencia + run_test "Verificación de balance del fee_collector después de la transferencia" \ + "(30_000 : nat)" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$Minter\"; subaccount = opt blob \"FeeCollector00000000000000000000\"}, + )'" + +else + echo "La próxima liberación de fondos no es para la categoría Founders. Realizando pruebas para Investors..." + + # Seleccionar una identidad Investor + dfx identity use 0000InvVesting + export Investor=$(dfx identity get-principal) + + run_test "Investor intenta transferir un valor por encima del disponible" \ + "(variant { Err = variant { InsufficientFunds = record { balance = $available_balance_investor : nat } } })" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder01\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 50000000001 : nat; + }, + )'" + + # Intentar transferir justo el valor disponible menos la comisión + run_test "Investor intenta transferir el valor disponible exacto menos la comisión" \ + "(variant { Ok = 9 : nat })" \ + "dfx canister call tour icrc1_transfer '( + record { + to = record { owner = principal \"$Founder01\"; subaccount = null; }; + fee = null; + memo = null; + from_subaccount = null; + created_at_time = null; + amount = 545454545454554 : nat; + }, + )'" + +¡ + run_test "Verificación de balance disponible de Investor después de la transferencia" \ + "(0 : nat)" \ + "echo $available_balance_investor" + + # Verificar el balance del receptor (Founder01) después de la transferencia + run_test "Verificación de balance de Founder01 después de la transferencia" \ + "($((available_balance_investor - fee)) : nat" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$Founder01\" }, + )'" + + # Verificar el balance del fee_collector después de la transferencia + run_test "Verificación de balance del fee_collector después de la transferencia" \ + "($((10_000 + fee)) : nat)" \ + "dfx canister call tour icrc1_balance_of '( + record { owner = principal \"$Minter\"; subaccount = opt blob \"FeeCollector00000000000000000000\"}, + )'" +fi + +mapfile -t next_release_entries < <(dfx canister call tour vestingsStatus | grep -E 'categoryName =|nextReleaseTime =' | awk -F '= ' '{print $2}' | sed 's/;//g' | tr -d '"') + +min=5000000000000000000 +nextCategory="" +now=$(date +%s) +currentCategory="" + +# Iterar sobre las entradas extraídas +for ((i=0; i<${#next_release_entries[@]}; i++)); do + entry="${next_release_entries[i]}" + + # Si la entrada es un categoryName, guardarlo temporalmente + if [[ "$entry" =~ ^[A-Za-z]+$ ]]; then + currentCategory="$entry" + + # Si la entrada es un timestamp, procesarlo + elif [[ "$entry" != "null" ]]; then + num=$(echo "$entry" | grep -oP '\d+(_\d+)*' | tr -d '_') + + if [[ -n "$num" && "$num" -lt "$min" ]]; then + min="$num" + nextCategory="$currentCategory" + fi + fi +done + +# Convertir timestamp a segundos +min_seconds=$((min / 1000000000)) +secondsWaiting=$((min_seconds - now)) + +# Imprimir resultados +echo "La proxima liberación de fondos corresponde a la categoria: $nextCategory" +echo "Segundos restantes: $secondsWaiting" + +wait_with_countdown $((secondsWaiting + 1)) + + diff --git a/scripts-sh/verLogo.html b/scripts-sh/verLogo.html new file mode 100644 index 0000000..15fc524 --- /dev/null +++ b/scripts-sh/verLogo.html @@ -0,0 +1,60 @@ + + + + + + SVG Base64 Test + + +

SVG Base64 Test

+

Si ves una imagen abajo, tu conversión fue exitosa:

+ Test Image + + diff --git a/tour/icrc1-custom.mo b/tour/icrc1-custom.mo new file mode 100644 index 0000000..434130a --- /dev/null +++ b/tour/icrc1-custom.mo @@ -0,0 +1,461 @@ +import ExperimentalCycles "mo:base/ExperimentalCycles"; +import Principal "mo:base/Principal"; +import Nat64 "mo:base/Nat64"; +import { now } "mo:base/Time"; +import { print } "mo:base/Debug"; +import Map "mo:map/Map"; +import { phash } "mo:map/Map"; + +import ICRC1 "mo:icrc1-mo/ICRC1"; +import ICRC2 "mo:icrc2-mo/ICRC2"; +import Types "types"; +import Tokenomic "tokenomic"; +import Vec "mo:vector"; +import Indexer "indexer"; +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Int "mo:base/Int"; +import Nat8 "mo:base/Nat8"; +import Buffer "mo:base/Buffer"; +import Nat "mo:base/Nat"; +import IC "../interfaces/ic-management-interface"; + +shared ({ caller = _owner }) actor class CustomToken( + // init_args1 : ICRC1.InitArgs, + // init_args2 : ICRC2.InitArgs, + ledgerArgs : Types.LedgerArgument, + customArgs : { + distribution : ?Tokenomic.InitialDistribution; + max_supply : ?Nat; + metadata : [(Text, Types.MetadataValue)]; + min_burn_amount : ?Nat; + }, +) = this { + + ///////////////////////////////// WARNING /////////////////////////////////////// + + // let NanosPerDay = 24 * 60 * 60 * 1_000_000_000; // Valor definitivo + let NanosPerDay = 2 * 1_000_000_000; // Valor para pruebas 1 dia = 4 segundo + + stable var distributionTimestamp : Int = 0; + + ////////////////////////////////////////////////////////////////////////////////// + + stable var icrc1_args : ?ICRC1.InitArgs = null; + stable var icrc2_args : ?ICRC2.InitArgs = null; + switch ledgerArgs { + case (#Init(initArgs)) { + icrc1_args := ?{ + decimals = initArgs.decimals; + advanced_settings = null; + fee = ?#Fixed(initArgs.transfer_fee); + minting_account = ?initArgs.minting_account; + fee_collector = initArgs.fee_collector_account; + logo = null; + max_accounts = null; + max_memo = initArgs.max_memo_length; + max_supply = customArgs.max_supply; + metadata = null; + min_burn_amount = customArgs.min_burn_amount; + name = ?initArgs.token_name; + permitted_drift = null; + settle_to_accounts = null; + symbol = ?initArgs.token_symbol; + transaction_window = null; + }; + icrc2_args := ?{ + advanced_settings = null; + fee = ?#Fixed(initArgs.transfer_fee); + max_allowance = null; + max_approvals = null; + max_approvals_per_account = null; + settle_to_approvals = null; + }; + + }; + + case (#Upgrade(_)) { + assert false; + }; + }; + + stable let icrc1_migration_state = ICRC1.init(ICRC1.initialState(), #v0_1_0(#id), icrc1_args, _owner); + + ///////////////////////////////////// Flags ////////////////////////////////////// + + stable var distributionComplete = false; + stable var vestingSchemes : [{ categoryName : Text; scheme : Tokenomic.VestingScheme; ended : Bool }] = []; + stable var isLedgerReady = false; + + ////// + + let #v0_1_0(#data(icrc1_state_current)) = icrc1_migration_state; + + private var _icrc1 : ?ICRC1.ICRC1 = null; + + private func get_icrc1_state() : ICRC1.CurrentState { + return icrc1_state_current; + }; + + private func get_icrc1_environment() : ICRC1.Environment { + { + get_time = null; + get_fee = null; + add_ledger_transaction = null; + can_transfer = null; + }; + }; + + func icrc1() : ICRC1.ICRC1 { + switch (_icrc1) { + case (null) { + let initclass : ICRC1.ICRC1 = ICRC1.ICRC1(?icrc1_migration_state, Principal.fromActor(this), get_icrc1_environment()); + _icrc1 := ?initclass; + initclass; + }; + case (?val) val; + }; + }; + + stable let icrc2_migration_state = ICRC2.init(ICRC2.initialState(), #v0_1_0(#id), icrc2_args, _owner); + + let #v0_1_0(#data(icrc2_state_current)) = icrc2_migration_state; + + private var _icrc2 : ?ICRC2.ICRC2 = null; + + private func get_icrc2_state() : ICRC2.CurrentState { + return icrc2_state_current; + }; + + private func get_icrc2_environment() : ICRC2.Environment { + { + icrc1 = icrc1(); + get_fee = null; + can_approve = null; + can_transfer_from = null; + }; + }; + + func icrc2() : ICRC2.ICRC2 { + switch (_icrc2) { + case (null) { + let initclass : ICRC2.ICRC2 = ICRC2.ICRC2(?icrc2_migration_state, Principal.fromActor(this), get_icrc2_environment()); + _icrc2 := ?initclass; + initclass; + }; + case (?val) val; + }; + }; + + stable var _indexer : ?Indexer.Indexer = null; + + func pushTrxToIndexer(trxResult : ICRC1.TransferResult) : async ICRC1.TransferResult { + switch (trxResult) { + case (#Err(_)) {}; + case (#Ok(index)) { + let local_transactions = icrc1().get_local_transactions(); + let _trx = Vec.get(local_transactions, index); + switch (_indexer) { + case (?indexer) { + indexer.on_transaction(_trx, index); + }; + case (null) {}; + }; + }; + }; + trxResult; + }; + + /////////////////// Deploy indexer ////////////////// + + func deploy_indexer() : async Principal { + switch _indexer { + case null { + ExperimentalCycles.add(2_000_000_000_000); + let indexer = await Indexer.Indexer(); + _indexer := ?indexer; + let indexerCanisterId = Principal.fromActor(indexer); + // Agregamos al _owner como controlador del indexer + await IC.addController(Principal.fromActor(indexer), _owner); + indexerCanisterId; + }; + case (?pid) { Principal.fromActor(pid) }; + }; + }; + + //////////////// Initial Distribution //////////////// + + func distribution(allocations : [Tokenomic.Allocation]) : async { #Ok; #Err : Text } { + if (distributionComplete) { return #Err("Distribution is already complete") }; + for (distItem in allocations.vals()) { + vestingSchemes := Array.tabulate<{ categoryName : Text; scheme : Tokenomic.VestingScheme; ended : Bool }>( + vestingSchemes.size() + 1, + func i = if (i == vestingSchemes.size()) { + { categoryName = distItem.categoryName; scheme = distItem.vestingScheme; ended = false }; + } else { + vestingSchemes[i]; + }, + ); + for ({ allocatedAmount; hasVesting; owner } in distItem.holders.vals()) { + let mintArgs = { + to : Types.Account = { owner; subaccount = null }; + amount = allocatedAmount; + memo = null; + created_at_time = ?Nat64.fromNat(Int.abs(now())); + }; + let minting_account = get_icrc1_state().minting_account; + ignore await* icrc1().mint(minting_account.owner, mintArgs); + + // Mapeo holder/amount para permitir o denegar transacciones durante periodo de vesting + if (Map.has(holdersVesting, phash, owner)) { + return #Err("Hay un mismo principal en mas de una categoría de distribución"); + }; + + if (hasVesting) { + ignore Map.put( + holdersVesting, + phash, + owner, + { value = allocatedAmount; schemeIndex = vestingSchemes.size() - 1 }, + ); + }; + }; + }; + distributionComplete := true; + distributionTimestamp := now(); + #Ok; + }; + + ////// Deploy de canister indexer y distribucion inicial /////// + + public shared ({ caller }) func initialize() : async { #Ok; #Err : Text } { + assert (caller == _owner); + let indexerCanisterId = await deploy_indexer(); + print("Indexer canister deployed at " # debug_show (indexerCanisterId)); + switch ledgerArgs { + case (#Init(_)) { + switch (customArgs.distribution) { + case (?dist) { + return await distribution(dist.allocations); + }; + case (_) { return #Ok }; + }; + }; + case (_) { return #Ok }; + }; + + }; + + /////////////////////// vesting validations ///////////////////// + + stable let holdersVesting = Map.new(); + + func calculateBlockedAmount(p : Principal) : Nat { + switch (Map.get(holdersVesting, phash, p)) { + case null { return 0 }; + case (?{ value; schemeIndex }) { + if (vestingSchemes[schemeIndex].ended) { + return 0; + } else { + switch (vestingSchemes[schemeIndex].scheme) { + case (#timeBasedVesting(scheme)) { + let { period } = getCurrentPeriodVesting(scheme); + + if (period == 0) { + return value; + } else { + return value - ((value * period) / Nat8.toNat(scheme.intervalQty)); + }; + }; + case (_) { return 0 } // Otros esquemas a implementar + }; + value; + }; + }; + }; + }; + + func checkVestingRestrictions(caller : Principal, trx : ICRC1.TransferArgs) : { + #Ok; + #Err : Types.TransferError; + } { + // TODO Se puede agregar un valor aleatorio y acotado a cada usuario con vesting para evitar + // que todos los usuarios de una misma categoria queden con sus tokens liberados al mismo tiempo + let balance = icrc1().balance_of({ owner = caller; subaccount = null }); + let blocked_amount = calculateBlockedAmount(caller); + if (blocked_amount == 0) { return #Ok }; + if (balance >= blocked_amount + trx.amount + icrc1().fee()) { + #Ok; + } else { + #Err(#VestingRestriction({ blocked_amount; available_amount = balance - blocked_amount })); + }; + }; + + // Custom functions + + public query func indexerCanister() : async ?Principal { + switch (_indexer) { + case null { null }; + case (?indexer) { + ?Principal.fromActor(indexer); + }; + }; + }; + + public shared query ({ caller }) func balance(subaccount : ?Blob) : async Nat { + icrc1().balance_of({ owner = caller; subaccount }); + }; + + // public shared ({ caller }) func available(): async Nat { + // icrc1().balance_of({ owner = caller; subaccount = null }) - get; + // }; + + func getCurrentPeriodVesting(scheme : Tokenomic.TimeBasedVesting) : { period : Nat; cliff : Int } { + let currentTime = Int.abs(now()); + let cliff = switch (scheme.cliff) { + case null { distributionTimestamp }; + case (?c) { c * 1_000_000_000 }; + }; + var period = Int.abs( + if (currentTime < cliff) { 0 } else { + let p = ((currentTime - cliff) / (scheme.intervalDuration * NanosPerDay) + 1); + if (p <= Nat8.toNat(scheme.intervalQty)) { p } else { Nat8.toNat(scheme.intervalQty) }; + } + ); + return { period; cliff }; + }; + + public query func vestingsStatus() : async [Tokenomic.VestingState] { + let states = Buffer.fromArray([]); + for (vst in vestingSchemes.vals()) { + switch (vst.scheme) { + case (#timeBasedVesting(scheme)) { + let { period; cliff } = getCurrentPeriodVesting(scheme); + + let vState : Tokenomic.VestingState = { + categoryName = vst.categoryName; + currentPeriodOverTotal = (period, Nat8.toNat(scheme.intervalQty)); + isBeforeCliff = period == 0; + isFullyVested = period == Nat8.toNat(scheme.intervalQty); + nextReleaseTime = if (period == Nat8.toNat(scheme.intervalQty)) { null } else { + ?(cliff + period * scheme.intervalDuration * NanosPerDay); + }; + remainingAmount = 0; // TODO + vestedAmount = 0; // TODO + }; + states.add(vState); + + }; + case _ {}; + }; + }; + Buffer.toArray(states); + }; + + /// Functions for the ICRC1 token standard + + public shared query func is_ledger_ready() : async Bool { + isLedgerReady; + }; + + public shared query func icrc1_name() : async Text { + icrc1().name(); + }; + + public shared query func icrc1_symbol() : async Text { + icrc1().symbol(); + }; + + public shared query func icrc1_decimals() : async Nat8 { + icrc1().decimals(); + }; + + public shared query func icrc1_fee() : async ICRC1.Balance { + icrc1().fee(); + }; + + public shared query func icrc1_metadata() : async [ICRC1.MetaDatum] { + icrc1().metadata(); + }; + + public shared query func icrc1_total_supply() : async ICRC1.Balance { + icrc1().total_supply(); + }; + + public shared query func icrc1_minting_account() : async ?ICRC1.Account { + ?icrc1().minting_account(); + }; + + public shared query func icrc1_balance_of(args : ICRC1.Account) : async ICRC1.Balance { + icrc1().balance_of(args); + }; + + public shared query func icrc1_supported_standards() : async [ICRC1.SupportedStandard] { + icrc1().supported_standards(); + }; + + public shared ({ caller }) func icrc1_transfer(args : ICRC1.TransferArgs) : async Types.TransferResult { + switch (checkVestingRestrictions(caller, args)) { + case (#Err(e)) { return #Err(e) }; + case _ {}; + }; + let trxResult = await* icrc1().transfer(caller, args); + ignore pushTrxToIndexer(trxResult); + trxResult; + }; + + public shared ({ caller }) func mint(args : ICRC1.Mint) : async ICRC1.TransferResult { + let trxResult = await* icrc1().mint(caller, args); + await pushTrxToIndexer(trxResult); + }; + + public shared ({ caller }) func burn(args : ICRC1.BurnArgs) : async ICRC1.TransferResult { + let trxResult = await* icrc1().burn(caller, args); + await pushTrxToIndexer(trxResult); + }; + + public query func icrc2_allowance(args : ICRC2.AllowanceArgs) : async ICRC2.Allowance { + return icrc2().allowance(args.spender, args.account, false); + }; + + public shared ({ caller }) func icrc2_approve(args : ICRC2.ApproveArgs) : async Types.ApproveResponse { + switch (checkVestingRestrictions(caller, { args with to = args.spender } : ICRC1.TransferArgs)) { + case (#Err(#VestingRestriction(e))) { return #Err(#VestingRestriction(e)) }; + case _ {}; + }; + await* icrc2().approve(caller, args); + }; + + public shared ({ caller }) func icrc2_transfer_from(args : ICRC2.TransferFromArgs) : async ICRC2.TransferFromResponse { + switch (checkVestingRestrictions(args.from.owner, { args with from_subaccount = args.from.subaccount } : ICRC1.TransferArgs)) { + case (#Err(e)) { return #Err(e) }; + case _ {}; + }; + let trxResult = await* icrc2().transfer_from(caller, args); + switch trxResult { + case (#Err(_)) { trxResult }; + case (#Ok(index)) { await pushTrxToIndexer(#Ok(index)) }; + }; + }; + + public query func getTransactionRange(start : Nat, _end : ?Nat) : async [ICRC1.Transaction] { + let local_transactions = icrc1().get_local_transactions(); + let end = switch _end { + case null { + Vec.size(local_transactions); + }; + case (?val) { + if (val > Vec.size(local_transactions)) { Vec.size(local_transactions) } else { val }; + }; + }; + Array.tabulate(end - start, func i = Vec.get(local_transactions, start + i)); + }; + + // Deposit cycles into this canister. + public shared func deposit_cycles() : async () { + + let amount = ExperimentalCycles.available(); + let accepted = ExperimentalCycles.accept(amount); + assert (accepted == amount); + }; +}; diff --git a/tour/indexer.mo b/tour/indexer.mo new file mode 100644 index 0000000..1dbdfb8 --- /dev/null +++ b/tour/indexer.mo @@ -0,0 +1,130 @@ +import Principal "mo:base/Principal"; +import Map "mo:map/Map"; +// import { phash } "mo:map/Map"; +// import TrieMap "mo:base/TrieMap"; + +import ICRC1 "mo:icrc1-mo/ICRC1"; +import ICRC2 "mo:icrc2-mo/ICRC2"; +import { print } "mo:base/Debug"; +import Array "mo:base/Array"; + +/* + get_blocks : shared query GetBlocksRequest -> async GetBlocksResponse; + get_fee_collectors_ranges : shared query () -> async FeeCollectorRanges; + list_subaccounts : shared query ListSubaccountsArgs -> async [SubAccount]; + status : shared query () -> async Status; +*/ + + +shared ({caller = LedgerCanisterId}) actor class Indexer() = this { + + let ledger = actor( Principal.toText(LedgerCanisterId) ): actor { + icrc1_decimals: shared query () -> async Nat8; + icrc1_fee: shared query () -> async Nat; + // icrc1_metadata: shared query () -> async [ICRC1.MetaDatum]; + // icrc1_total_supply: shared query () -> async ICRC1.Balance; + icrc1_minting_account: shared query () -> async ?ICRC1.Account; + icrc1_balance_of: shared query ICRC1.Account -> async ICRC1.Balance; + icrc1_supported_standards: shared query () -> async [ICRC1.SupportedStandard]; + icrc1_transfer: shared ICRC1.TransferArgs -> async ICRC1.TransferResult; + mint: shared ICRC1.Mint -> async ICRC1.TransferResult; + burn: shared ICRC1.BurnArgs -> async ICRC1.TransferResult; + icrc2_allowance: query ICRC2.AllowanceArgs -> async ICRC2.Allowance; + icrc2_approve: shared ICRC2.ApproveArgs -> async ICRC2.ApproveResponse; + icrc2_transfer_from: shared ICRC2.TransferFromArgs -> async ICRC2.TransferFromResponse; + getTransactionRange: query (Nat, ?Nat) -> async [ICRC1.Transaction]; + }; + + type TokenTransferredListener = ICRC1.TokenTransferredListener; + type Account = {owner: Principal; subaccount: ?Blob}; + + stable let accountsTransactions = Map.new(); + stable var transactions: [ICRC1.Transaction] = []; + + private func pull_missing_transactions(): async () { + let _transactionsPulled: [ICRC1.Transaction] = await ledger.getTransactionRange(transactions.size(), null); + transactions := Array.tabulate( + transactions.size() + _transactionsPulled.size(), + func i = if (i < transactions.size()) { transactions[i] } else { _transactionsPulled[i - transactions.size()] } + ); + for (trx in _transactionsPulled.vals()) { + index_transaction(trx) + }; + }; + + private func index_transaction(trx: ICRC1.Transaction) { + let accounts = switch (trx.kind) { + case "TRANSFER" { + switch (trx.transfer) { + case null { [] }; + case (?data) { [data.from, data.to] } + }; + }; + case "MINT" { + + switch (trx.mint) { + case null { [] }; + case (?data) { [data.to] } + } + }; + case "BURN" { + switch (trx.burn) { + case null { [] }; + case (?data) { [data.from] } + } + }; + case _ { [] } + }; + for (account in accounts.vals() ){ + let trxsPrevious = Map.get(accountsTransactions, ICRC1.ahash, account); + switch (trxsPrevious) { + case null { + ignore Map.put(accountsTransactions, ICRC1.ahash, account, [trx]) + }; + case (?trxsPrevious) { + let updatedTrxs = Array.tabulate( + trxsPrevious.size() + 1, + func i = if (i == 0) { trx } else { trxsPrevious[i - 1] } + ); + ignore Map.put(accountsTransactions, ICRC1.ahash, account, updatedTrxs) + }; + }; + }; + }; + + public shared ({ caller }) func on_transaction(trx: ICRC1.Transaction, index: Nat) : () { + assert( caller == LedgerCanisterId); + assert( index >= transactions.size()); + if (index == transactions.size()) { + index_transaction(trx) + } else { + await pull_missing_transactions(); + }; + }; + + public query func get_account_transactions(account: Account): async [ICRC1.Transaction] { + switch (Map.get(accountsTransactions, ICRC1.ahash, account)){ + case null { [] }; + case (?trxs) { trxs }; + }; + }; + + public func icrc1_balance_of(a: ICRC1.Account): async ICRC1.Balance{ + await ledger.icrc1_balance_of(a) + }; + + // public query func get_account_transactions({max_results : Nat; start : ?Nat; account : Account}): async GetTransactionsResult{ + + // }; + + public query func ledger_id(): async Principal{ + LedgerCanisterId + }; + + + + + +} + + diff --git a/tour/minter-canister.mo b/tour/minter-canister.mo new file mode 100644 index 0000000..b02fbec --- /dev/null +++ b/tour/minter-canister.mo @@ -0,0 +1,168 @@ +import Principal "mo:base/Principal"; +import Blob "mo:base/Blob"; +import Text "mo:base/Text"; +import { now } "mo:base/Time"; +import Ledger "icrc1-custom"; +import ICRC1 "mo:icrc1-mo/ICRC1"; +// import { print } "mo:base/Debug"; +import Int "mo:base/Int"; +import Nat64 "mo:base/Nat64"; + +/// Este canister debe ser desplegado despues de Triourism y antes del Tour ledger. +// Luego de desplegado el Ledger ejecutar setLedger con el ledeger canister ID + +shared ({ caller = Deployer}) actor class Minter({triourismCanisterId: Principal}) = this { + + + //////////////// Si crece mover a un archivo de tipos /////// + type Account = {owner: Principal; subaccount: ?Blob}; + + public type FeesDispersionTable = { + toBurnPermille: Nat; + receivers: [{account: Account; permille: Nat}] // Permille es a mil lo que porcentage a 100 + }; + + //////////////// Si crece mover a un modulo de Utils //////// + + func feesDispersionValidate(d: FeesDispersionTable): Bool { + var result = d.toBurnPermille; + for (r in d.receivers.vals()) { + result += r.permille + }; + result < 1000; + }; + + func fee(): async Nat { + switch _fee { + case null { await LedgerActor.icrc1_fee() }; + case ( ?f ) { + _fee := ?f; + f + } + } + }; + + ///////////////////// Variables de estado /////////////////// + + let NULL_ADDRESS = "aaaaa-aa"; + + // let fees_collector_subaccount: ?Blob = ? "FeeCollector00000000000000000000"; // El Blob del subaccount tiene que medir 32 Bytes + // let pool_rewards: ?Blob = ? "PoolRewards00000000000000000000"; + + // var pool_rewards_balance = 0; + stable var _fee: ?Nat = null; + + func pool_rewards_account(): Account { + {owner = Principal.fromActor(this); subaccount = ? "PoolRewards00000000000000000000"} + }; + + func fees_collector_account(): Account { + {owner = Principal.fromActor(this); subaccount = ? "FeeCollector00000000000000000000"} + }; + + + stable var LedgerActor = actor(NULL_ADDRESS): Ledger.CustomToken; + stable let TriourismCanisterId = triourismCanisterId; + stable var OldLedgerActor = actor(NULL_ADDRESS): Ledger.CustomToken; // Junto con restorePreviousLedger posiblemente innecesario + stable var feesDispersionTable: FeesDispersionTable = {toBurnPermille = 0; receivers = []}; + + + /////////////////////// Settings ////////////////////////////////////////////////////////////////// + + public shared ({ caller }) func setLedger(lerdgerCanisterId: Principal): async {#Ok; #Err: Text}{ + assert(caller == Deployer); + OldLedgerActor := LedgerActor; + LedgerActor := actor(Principal.toText(lerdgerCanisterId)): Ledger.CustomToken; + _fee := ?(await LedgerActor.icrc1_fee()); + #Ok + }; + + public shared ({ caller }) func restorePreviousLedger(): async {#Ok; #Err: Text}{ + assert(caller == Deployer); + if (Principal.fromActor(OldLedgerActor) != Principal.fromText("aaaaa-aa")) { + LedgerActor := OldLedgerActor; + OldLedgerActor := actor(NULL_ADDRESS): Ledger.CustomToken; + _fee := ?(await OldLedgerActor.icrc1_fee()); + return #Ok + }; + #Err("No hay registros de ledgers configurados previamente al actual") + }; + + public shared ({ caller }) func setFeesDispersionTable(d: FeesDispersionTable): async {#Ok; #Err: Text}{ + assert (caller == Deployer); + if ( not feesDispersionValidate(d)) { return #Err("La suma de todos los items debe ser menor o igual a 1000") }; + feesDispersionTable := d; + #Ok + }; + + func ledgerReady(): Bool { + Principal.fromActor(LedgerActor) != Principal.fromText(NULL_ADDRESS) + }; + + ////////////////////////////////////// Getters Fees dispersion section ///////////////////////////////////////////// + + public query func getFeeCollector(): async Account { + fees_collector_account() + }; + + public shared func getFeeCollectorBalance(): async Nat { + await LedgerActor.icrc1_balance_of(fees_collector_account()) + }; + + public shared ({ caller }) func getFeesDispersionTable(): async FeesDispersionTable{ + assert(caller == Deployer); + feesDispersionTable + }; + + public query func getLedgerCanisterId(): async Principal { + Principal.fromActor(LedgerActor); + }; + + /////////////////////////////////////// Mint section //////////////////////////////////////////////////////////////// + + // public shared ({ caller }) func mintRewards(args: ICRC1.Mint): async ICRC1.TransferResult{ + // print("Minter recibiendo llamada"); + // assert(caller == TriourismCanisterId and ledgerReady()); + // print("Llamada aceptada"); + // await LedgerActor.mint(args); + // }; + + public shared ({ caller }) func issueRewards({accounts: [Account]; amount : Nat64}): async {#Ok; #Err}{ + assert(caller == TriourismCanisterId and ledgerReady()); + let args = { + amount = Nat64.toNat(amount); + memo: ?Blob = null; + created_at_time = ? Nat64.fromNat(Int.abs(now())); + }; + let pool_rewards_balance = await LedgerActor.icrc1_balance_of(pool_rewards_account()); + if(pool_rewards_balance >= accounts.size() * (args.amount + (await fee()))){ + let from_subaccount: ?ICRC1.Subaccount = pool_rewards_account().subaccount; + for (to in accounts.vals()){ + switch (await LedgerActor.icrc1_transfer({ + args with + fee = null; + from_subaccount; + to; + })) { + case (#Err(_)) { ignore LedgerActor.mint({args with to})}; + case _ {} + }; + } + } else { + for (to in accounts.vals()){ + ignore await LedgerActor.mint({args with to}) + } + }; + + #Ok + + }; + + + + + + + + +} \ No newline at end of file diff --git a/tour/tokenomic.mo b/tour/tokenomic.mo new file mode 100644 index 0000000..e9001f5 --- /dev/null +++ b/tour/tokenomic.mo @@ -0,0 +1,77 @@ +import Nat8 "mo:base/Nat8"; + +module { + + public type InitialDistribution = { + allocations : [Allocation]; + }; + + public type Allocation = { + categoryName : Text; + holders : [InitialHolder]; + vestingScheme : VestingScheme; + }; + + public type VestingScheme = { + #timeBasedVesting : TimeBasedVesting; + #mintBasedVesting : MintBasedVesting; + }; + + public type VestingState = { + categoryName: Text; + isBeforeCliff : Bool; + isFullyVested : Bool; // Si ya se liberaron todos los tokens (currentTime >= endTime) + currentPeriodOverTotal: (Nat, Nat); + vestedAmount : Nat; // Cantidad total liberada hasta ahora + remainingAmount : Nat; // Cantidad aún bloqueada + nextReleaseTime : ?Int; // Timestamp del próximo release (null si ya terminó) + }; + + public type InitialHolder = { + owner : Principal; + allocatedAmount : Nat; + hasVesting : Bool; + }; + + public type TimeBasedVesting = { + cliff : ?Int; // Comienzo del periodo de vesting. Si es null se toma la fecha del deploy. Timestamp seg + // duration : Nat; // Duración del periodo de vesting desde el cliff // comentado por redundante + // releaseRate : Nat; // Cantidad de tokens a liberar por periodo luego del periodo de vesting. Opcion de nombre maxAmountPerRelease + // El releaseRate se calcularia como ```amount / (intervalQty + 1)``` + intervalDuration : Nat; // Intervalo de tiempo en dias entre cada liberación + intervalQty : Nat8; // Cantidad de intervalos. ```duration = intervalQty * releaseInterval``` + }; + + public type VestingRule = { + timeBasedVesting : ?TimeBasedVesting; + mintBasedVesting : ?MintBasedVesting; + }; + + ///// Revisar regla //////////////////// + + public type MintBasedVesting = { + triggers : [{ totalSupply : Nat; releaseAmount : Nat }]; + withdrawalRatio : Nat; //relacion entre el totalSupply actual y el maximo que se puede retirar en un solo trigger + }; + + public func mintBasedVestingValidate(mintBasedVesting : MintBasedVesting) : Bool { + var lastTrigger = { totalSupply = 0; releaseAmount = 0 }; + let ratio = if (mintBasedVesting.withdrawalRatio < 500) { + 500; + } else { + mintBasedVesting.withdrawalRatio; + }; + for (t in mintBasedVesting.triggers.vals()) { + if ( + t.totalSupply <= lastTrigger.totalSupply or + t.releaseAmount <= lastTrigger.releaseAmount or + t.totalSupply < t.releaseAmount * ratio + ) { + return false; + }; + lastTrigger := t; + }; + true; + }; + +}; diff --git a/tour/types.mo b/tour/types.mo new file mode 100644 index 0000000..8fe8e4e --- /dev/null +++ b/tour/types.mo @@ -0,0 +1,96 @@ +import ICRC1 "mo:icrc1-mo/ICRC1"; +import ICRC2 "mo:icrc2-mo/ICRC2"; + +module { + + // Custom Errors + + public type TxIndex = Nat; + public type TransferResult = { + #Ok : TxIndex; + #Err : TransferError; + }; + public type TransferError = ICRC1.TransferError or { + #VestingRestriction : { + blocked_amount : Nat; + available_amount : Nat; + }; + }; + + public type ApproveResponse = { + #Ok : Nat; + #Err : ApproveError + }; + + public type ApproveError = ICRC2.ApproveError or { + #VestingRestriction : { + blocked_amount : Nat; + available_amount : Nat; + }; + }; + //////////////////////////////////////// + + + + public type Account = { owner : Principal; subaccount : ?Blob }; + + public type ArchiveOptions = { + num_blocks_to_archive : Nat64; + max_transactions_per_response : ?Nat64; + trigger_threshold : Nat64; + more_controller_ids : ?[Principal]; + max_message_size_bytes : ?Nat64; + cycles_for_archive_creation : ?Nat64; + node_max_memory_size_bytes : ?Nat64; + controller_id : Principal; + }; + public type FeatureFlags = { icrc2 : Bool }; + + public type LedgerArgument = { #Upgrade : ?UpgradeArgs; #Init : InitArgs }; + + public type ChangeArchiveOptions = { + num_blocks_to_archive : ?Nat64; + max_transactions_per_response : ?Nat64; + trigger_threshold : ?Nat64; + more_controller_ids : ?[Principal]; + max_message_size_bytes : ?Nat64; + cycles_for_archive_creation : ?Nat64; + node_max_memory_size_bytes : ?Nat64; + controller_id : ?Principal; + }; + + public type ChangeFeeCollector = { #SetTo : Account; #Unset }; + + public type UpgradeArgs = { + change_archive_options : ?ChangeArchiveOptions; + token_symbol : ?Text; + transfer_fee : ?Nat; + metadata : ?[(Text, MetadataValue)]; + change_fee_collector : ?ChangeFeeCollector; + max_memo_length : ?Nat; + token_name : ?Text; + feature_flags : ?FeatureFlags; + }; + + public type InitArgs = { + decimals : Nat8; + token_symbol : Text; + max_supply : ?Nat; + transfer_fee : Nat; + metadata : [(Text, MetadataValue)]; + minting_account : Account; + initial_balances : [(Account, Nat)]; + fee_collector_account : ?Account; + archive_options : ArchiveOptions; + max_memo_length : ?Nat; + token_name : Text; + feature_flags : ?FeatureFlags; + }; + + public type MetadataValue = { + #Int : Int; + #Nat : Nat; + #Blob : Blob; + #Text : Text; + }; +};