// charge an object. Deduct from an object, create one or more objects and // associated to one or more wallets func (c *ObjectController) Charge(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // parse body var body objectChargeBody if err := c.ParseJsonBody(req, &body); err != nil { services.Res(res).Error(400, "invalid_body", "request body is invalid or malformed. Expects valid json body") return } // TODO: get client id from access token clientID := "kl14zFDq4SHlmmmVNHgLtE0LqCo8BTjyShOH" // get db transaction object dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // get service service, _, _ := models.FindServiceByClientId(dbTx, clientID) // ensure object ids is not empty if len(body.IDS) == 0 { services.Res(res).ErrParam("ids").Error(400, "invalid_parameter", "provide one or more object ids to charge") return } // ensure object ids length is less than 100 if len(body.IDS) > 100 { services.Res(res).ErrParam("ids").Error(400, "invalid_parameter", "only a maximum of 100 objects can be charge at a time") return } // ensure destination wallet is provided if c.validate.IsEmpty(body.DestinationWalletID) { services.Res(res).ErrParam("wallet_id").Error(400, "invalid_parameter", "destination wallet id is reqired") return } // ensure amount is provided if body.Amount < MinimumObjectUnit { services.Res(res).ErrParam("amount").Error(400, "invalid_parameter", fmt.Sprintf("amount is below the minimum charge limit. Mininum charge limit is %.8f", MinimumObjectUnit)) return } // if meta is provided, ensure it is not greater than the limit size if !c.validate.IsEmpty(body.Meta) && len([]byte(body.Meta)) > MaxMetaSize { services.Res(res).ErrParam("meta").Error(400, "invalid_parameter", fmt.Sprintf("Meta contains too much data. Max size is %d bytes", MaxMetaSize)) return } // ensure destination wallet exists wallet, found, err := models.FindWalletByObjectID(dbTx, body.DestinationWalletID) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "api_error", "api_error") return } else if !found { dbTx.Rollback() services.Res(res).ErrParam("wallet_id").Error(404, "not_found", "wallet_id not found") return } // find all objects objectsFound, err := models.FindAllObjectsByObjectID(dbTx, body.IDS) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "api_error", "api_error") return } // ensure all objects exists if len(objectsFound) != len(body.IDS) { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(404, "object_error", "one or more objects do not exist") return } // sort object by balance in descending order sort.Sort(services.ByObjectBalance(objectsFound)) // objects to charge objectsToCharge := []models.Object{} // validate each object // check open status (timed and pin) // collect the required objects to sufficiently // complete a charge from the list of found objects for _, object := range objectsFound { // as long as the total balance of objects to be charged is not above charge amount // keep setting aside objects to charge from. // once we have the required objects to cover charge amount, stop processing other objects if TotalBalance(objectsToCharge) < body.Amount { objectsToCharge = append(objectsToCharge, object) } else { break } // ensure service is the issuer of object if object.Service.ObjectID != service.ObjectID { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: service cannot charge an object not issued by it", object.ObjectID)) return } // ensure object is open if !object.Open { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: object is not opened and cannot be charged", object.ObjectID)) return } else { // for object with open_timed open method, ensure time is not passed if object.OpenMethod == models.ObjectOpenTimed { objectOpenTime := services.UnixToTime(object.OpenTime).UTC() now := time.Now().UTC() if now.After(objectOpenTime) { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: object open time period has expired", object.ObjectID)) return } } // for object with open_pin open method, ensure pin is provided and // it matches. Pin should be found in the optional pin object of the request body if object.OpenMethod == models.ObjectOpenPin { if pin, found := body.Pins[object.ObjectID]; found { // ensure pin provided matches objects pin if !services.BcryptCompare(object.OpenPin, strconv.Itoa(pin)) { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: pin provided to open object is invalid", object.ObjectID)) return } } else { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: object pin not found in pin parameter of request body", object.ObjectID)) return } } } } totalObjectsBalance := TotalBalance(objectsToCharge) // ensure total balance of objects to charge is sufficient for charge amount if totalObjectsBalance < body.Amount { dbTx.Rollback() services.Res(res).ErrParam("amount").Error(402, "invalid_parameter", fmt.Sprintf("object%s total balance not sufficient to cover charge amount", services.SIfNotZero(len(body.IDS)))) return } lastObj := objectsToCharge[len(objectsToCharge)-1] // if there is excess, the last object is always the supplement object. // deduct from last object's balance, update object and remove it from the objectsToCharge list if totalObjectsBalance > body.Amount { lastObj.Balance = totalObjectsBalance - body.Amount objectsToCharge = objectsToCharge[0 : len(objectsToCharge)-1] dbTx.Save(&lastObj) } // delete the objects to charge for _, object := range objectsToCharge { dbTx.Delete(&object) } // create new object. set balance to charge balance // generate a pin countryCallCode := config.CurrencyCallCodes[strings.ToUpper(service.Identity.BaseCurrency)] newPin, err := services.NewObjectPin(strconv.Itoa(countryCallCode)) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "api_error", "server error") return } newObj := NewObject(newPin, models.ObjectValue, service, wallet, body.Amount, body.Meta) err = models.CreateObject(dbTx, &newObj) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "api_error", "server error") return } dbTx.Save(&newObj).Commit() services.Res(res).Json(newObj) }
// generate and return client_credentials token func (c *AuthController) GetClientCredentialToken(res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // get base64 encoded credentials base64Credential := services.StringSplit(req.Header.Get("Authorization"), " ")[1] base64CredentialDecoded := services.DecodeB64(base64Credential) credentials := services.StringSplit(base64CredentialDecoded, ":") // check if requesting client is a back service id if credentials[0] == BackOfficeId && credentials[1] == BackOfficeSecret { exp := int64(0) token, err := createJWTToken("", true, exp) if err != nil { log.Error(err) services.Res(res).Error(500, "", "server error") return } // create and save new token newToken := models.Token{ Token: token, Type: "bearer", ExpiresIn: time.Time{}, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } // persist token err = models.CreateToken(db.GetPostgresHandle(), &newToken) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } services.Res(res).Json(newToken) return } // find service by client id service, found, err := models.FindServiceByClientId(db.GetPostgresHandle(), credentials[0]) if !found && err == nil { log.Error(err) services.Res(res).Error(404, "", "service not found") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // compare secret if credentials[1] != service.ClientSecret { log.Error(err) services.Res(res).Error(401, "", "service credentials are invalid. ensure client id and secret are valid") return } // create access token exp := time.Now().Add(time.Hour * 1) token, err := createJWTToken(service.ObjectID, false, exp.UTC().Unix()) if err != nil { log.Error(err) services.Res(res).Error(500, "", "server error") return } // create and save new token newToken := models.Token{ Service: service, Token: token, Type: "bearer", ExpiresIn: exp.UTC(), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } // persist token err = models.CreateToken(db.GetPostgresHandle(), &newToken) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } respObj, _ := services.StructToJsonToMap(newToken) respObj["service"] = services.DeleteKeys(respObj["service"].(map[string]interface{}), "client_id", "client_secret") services.Res(res).Json(respObj) }
// create object controller func (c *ObjectController) Create(res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // parse body var body objectCreateBody if err := c.ParseJsonBody(req, &body); err != nil { services.Res(res).Error(400, "invalid_body", "request body is invalid or malformed. Expects valid json body") return } // TODO: get client id from access token clientID := "kl14zFDq4SHlmmmVNHgLtE0LqCo8BTjyShOH" // get db transaction object dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // get service service, _, _ := models.FindServiceByClientId(dbTx, clientID) // ensure service is an issuer if !service.Identity.Issuer { dbTx.Rollback() services.Res(res).Error(401, "unauthorized_service", "service is not an issuer") return } // type is required if validator.IsNull(body.Type) { dbTx.Rollback() services.Res(res).Error(400, "missing_parameter", "Missing required field: type") return } // ensure type is valid if strings.ToLower(body.Type) != models.ObjectValue && strings.ToLower(body.Type) != models.ObjectValueless { dbTx.Rollback() services.Res(res).Error(400, "invalid_type", "type can only be obj_value or obj_valueless") return } // wallet id is required if validator.IsNull(body.WalletID) { dbTx.Rollback() services.Res(res).Error(400, "missing_parameter", "Missing required field: wallet_id") return } // ensure number of objects is greater than 0 if body.NumberOfObjects < 1 { dbTx.Rollback() services.Res(res).Error(400, "invalid_number_objects", "number_objects must be atleast 1 but not more than 100") return } // ensure number of objects is not greater than 100(the limit) if body.NumberOfObjects > 100 { dbTx.Rollback() services.Res(res).Error(400, "invalid_number_objects", "number_objects must not be more than 100") return } // for object of value, validate unit per object if strings.ToLower(body.Type) == models.ObjectValue { if body.BalancePerObject == 0 { dbTx.Rollback() services.Res(res).Error(400, "missing_parameter", "Missing required field: unit_per_object") return } if body.BalancePerObject < MinimumObjectUnit { dbTx.Rollback() services.Res(res).Error(400, "invalid_unit_per_object", "unit_per_object must be equal or greater than the minimum object unit which is 0.00000001") return } soulBalanceRequired := float64(body.NumberOfObjects) * body.BalancePerObject if service.Identity.SoulBalance < soulBalanceRequired { dbTx.Rollback() services.Res(res).Error(400, "insufficient_soul_balance", fmt.Sprintf("not enough soul balance to create object(s). Requires %.2f soul balance", soulBalanceRequired)) return } // update services soul balance service.Identity.SoulBalance = service.Identity.SoulBalance - soulBalanceRequired } // if meta is provided, ensure it is not greater than the limit size if !c.validate.IsEmpty(body.Meta) && len([]byte(body.Meta)) > MaxMetaSize { dbTx.Rollback() services.Res(res).Error(400, "invalid_meta_size", fmt.Sprintf("Meta contains too much data. Max size is %d bytes", MaxMetaSize)) return } // ensure wallet exists wallet, found, err := models.FindWalletByObjectID(dbTx, body.WalletID) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } else if !found { dbTx.Rollback() services.Res(res).Error(404, "invalid_wallet", "wallet_id is unknown") return } // ensure service owns the wallet if service.Identity.ObjectID != wallet.Identity.ObjectID { dbTx.Rollback() services.Res(res).Error(401, "invalid_wallet", "wallet is not owned by this service. Use a wallet created by this service") return } // create objects allNewObjects := []models.Object{} for i := 0; i < body.NumberOfObjects; i++ { // generate a pin countryCallCode := config.CurrencyCallCodes[strings.ToUpper(service.Identity.BaseCurrency)] newPin, err := services.NewObjectPin(strconv.Itoa(countryCallCode)) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } newObj := NewObject(newPin, body.Type, service, wallet, body.BalancePerObject, body.Meta) err = models.CreateObject(dbTx, &newObj) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } allNewObjects = append(allNewObjects, newObj) } // update identity's soul balance dbTx.Save(service.Identity).Commit() services.Res(res).Json(allNewObjects) }