// get a service func (c *ServiceController) Get(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { service, found, err := models.FindServiceByObjectID(db.GetPostgresHandle(), params["id"]) if !found { services.Res(res).Error(404, "not_found", "service was not found") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } respObj, _ := services.StructToJsonToMap(service) services.Res(res).Json(respObj) }
// get an identity func (c *IdentityController) Get(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { identity, found, err := models.FindIdentityByObjectID(db.GetPostgresHandle(), params["id"]) if !found { services.Res(res).Error(404, "not_found", "identity was not found") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // create response respObj, _ := services.StructToJsonToMap(identity) if identity.Issuer { respObj["soul_balance"] = identity.SoulBalance } services.Res(res).Json(respObj) }
// open/unlock a wallet func (c *WalletController) Open(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { // TODO: get from access token // authorizing wallet id authWalletID := "55c679145fe09c74ed000001" dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // get wallet wallet, found, err := models.FindWalletByObjectID(dbTx, params["id"]) if !found { dbTx.Rollback() services.Res(res).Error(404, "not_found", "wallet not found") return } else if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure wallet matches authorizing wallet if wallet.ObjectID != authWalletID { dbTx.Rollback() services.Res(res).Error(401, "unauthorized", "client does not have permission to access wallet") return } // update lock state to false wallet.Lock = false // save and commit dbTx.Save(&wallet).Commit() services.Res(res).Json(wallet) }
// lock sets set the 'open' property of an object to false. Also resets all fields used by // all open methods func (c *ObjectController) Lock(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // TODO: get from access token // authorizing wallet id authWalletID := "55c679145fe09c74ed000001" dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // get the object object, found, err := models.FindObjectByObjectID(dbTx, params["id"]) if !found { dbTx.Rollback() services.Res(res).Error(404, "not_found", "object was not found") return } else if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure object belongs to authorizing wallet if object.Wallet.ObjectID != authWalletID { dbTx.Rollback() services.Res(res).Error(401, "unauthorized", "objects: object does not belong to authorizing wallet") return } // clear open related fields of object clearOpen(&object) // save update and commit dbTx.Save(&object).Commit() services.Res(res).Json(object) }
// get an object by its id or pin func (c *ObjectController) Get(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { dbObj := db.GetPostgresHandle() object, found, err := models.FindObjectByObjectIDOrPin(dbObj, params["id"]) if !found { services.Res(res).Error(404, "not_found", "object was not found") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // construct response. remove private fields respObj, _ := services.StructToJsonToMap(object) delete(respObj["wallet"].(map[string]interface{})["identity"].(map[string]interface{}), "soul_balance") delete(respObj["wallet"].(map[string]interface{})["identity"].(map[string]interface{}), "email") delete(respObj["service"].(map[string]interface{})["identity"].(map[string]interface{}), "soul_balance") delete(respObj["service"].(map[string]interface{})["identity"].(map[string]interface{}), "email") services.Res(res).Json(respObj) }
// create an identity func (c *IdentityController) Create(res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { // parse request body var body identityCreateBody 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 } // full name is required if validator.IsNull(body.FullName) { services.Res(res).Error(400, "missing_parameter", "Missing required field: full_name") return } // email is required if validator.IsNull(body.Email) { services.Res(res).Error(400, "missing_parameter", "Missing required field: email") return } // email is required if !validator.IsEmail(body.Email) { services.Res(res).Error(400, "invalid_email", "email is invalid") return } // create identity newIdentity := models.Identity{ ObjectID: bson.NewObjectId().Hex(), FullName: body.FullName, Email: body.Email, } // if request is from Issuer controller, set issuer field to true if d := req.GetData("isIssuer"); d != nil && d.(bool) { // object is required if validator.IsNull(body.ObjectName) { services.Res(res).Error(400, "missing_parameter", "Missing required field: object_name") return } // base currency is required if validator.IsNull(body.BaseCurrency) { services.Res(res).Error(400, "missing_parameter", "Missing required field: base_currency") return } // base currency must be supported if !services.StringInStringSlice(config.SupportedBaseCurrencies, body.BaseCurrency) { services.Res(res).Error(400, "invalid_base_currency", "base currency is unknown") return } newIdentity.Issuer = true newIdentity.ObjectName = body.ObjectName newIdentity.BaseCurrency = body.BaseCurrency } err := models.CreateIdentity(db.GetPostgresHandle(), &newIdentity) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // create response respObj, _ := services.StructToJsonToMap(newIdentity) if newIdentity.Issuer { respObj["soul_balance"] = newIdentity.SoulBalance } services.Res(res).Json(respObj) }
// renew an issuer identity soul func (c *IdentityController) RenewSoul(res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { // parse request body var body soulRenewBody 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 } // identity id is required if validator.IsNull(body.IdentityId) { services.Res(res).Error(400, "missing_parameter", "Missing required field: identity_id") return } // soul balance is required if body.SoulBalance == 0 { services.Res(res).Error(400, "missing_parameter", "Missing required field: soul_balance") return } // soul balance must be greater than zero if body.SoulBalance < MinimumObjectUnit { services.Res(res).Error(400, "invalid_soul_balance", "Soul balance must be equal or greater than minimum object unit which is 0.00000001") return } // ensure identity exists identity, found, err := models.FindIdentityByObjectID(db.GetPostgresHandle(), body.IdentityId) if !found { services.Res(res).Error(404, "invalid_identity", "identity_id is unknown") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure identity is an issuer if !identity.Issuer { services.Res(res).Error(400, "invalid_identity", "identity is not an issuer") return } // add to soul balance newIdentity, err := models.AddToSoulByObjectID(db.GetPostgresHandle(), identity.ObjectID, body.SoulBalance) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // create response, hide some fields respObj, _ := services.StructToJsonToMap(newIdentity) if newIdentity.Issuer { respObj["soul_balance"] = newIdentity.SoulBalance } services.Res(res).Json(respObj) }
// ensures current request has an `Authorization` header func MustHaveAuthHeader(res http.ResponseWriter, arc services.AuxRequestContext, log *config.CustomLog) { if arc.Header.Get("Authorization") == "" { services.Res(res).Error(401, "invalid_request", "missing authorization header field") } }
// create a service func (c *ServiceController) Create(res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { // parse request body var body createBody if err := c.ParseJsonBody(req, &body); err != nil { services.Res(res).Error(400, "invalid_client", "request body is invalid or malformed. Expects valid json body") return } // name is required if c.validate.IsEmpty(body.FullName) { services.Res(res).Error(400, "missing_parameter", "Missing required field: full_name") return } // full name must have max of 60 characters if !validator.StringLength(body.FullName, "1", "60") { services.Res(res).Error(400, "invalid_full_name", "full_name character limit is 60") return } // service name is required if c.validate.IsEmpty(body.ServiceName) { services.Res(res).Error(400, "missing_parameter", "Missing required field: service_name") return } // service name must have max of 30 characters if !validator.StringLength(body.ServiceName, "1", "60") { services.Res(res).Error(400, "invalid_service_name", "service_name character limit is 60") return } // description is required if c.validate.IsEmpty(body.Description) { services.Res(res).Error(400, "missing_parameter", "Missing required field: description") return } // description must have max of 140 characters if !validator.StringLength(body.Description, "1", "140") { services.Res(res).Error(400, "invalid_description", "description character limit is 140") return } // email is required if c.validate.IsEmpty(body.Email) { services.Res(res).Error(400, "missing_parameter", "Missing required field: email") return } // email must be valid if !validator.IsEmail(body.Email) { services.Res(res).Error(400, "invalid_email", "email is invalid") return } // create identity newIdentity := &models.Identity{ ObjectID: bson.NewObjectId().Hex(), FullName: body.FullName, Email: body.Email, } // start db transaction dbTx := db.GetPostgresHandle().Begin() if err := models.CreateIdentity(dbTx, newIdentity); err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // create client credentials clientId := services.GetRandString(services.GetRandNumRange(32, 42)) clientSecret := services.GetRandString(services.GetRandNumRange(32, 42)) // create new service object newService := models.Service{ ObjectID: bson.NewObjectId().Hex(), Name: body.ServiceName, Description: body.Description, ClientID: clientId, ClientSecret: clientSecret, Identity: newIdentity, } // create service err := models.CreateService(dbTx, &newService) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // commit db transaction dbTx.Commit() // send response respObj, _ := services.StructToJsonToMap(newService) services.Res(res).Json(respObj) }
// 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) }
// open an object for charge/consumption. An object opened in this method // will be consumable without restriction func (c *ObjectController) Open(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // TODO: get from access token // authorizing wallet id authWalletID := "55c679145fe09c74ed000001" // parse body var body objectOpenBody 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 } // ensure open method is provided if c.validate.IsEmpty(body.OpenMethod) { services.Res(res).Error(400, "invalid_parameter", "open_method: open method is required") return } // ensure a known open method is provided if body.OpenMethod != "open" && body.OpenMethod != "open_timed" && body.OpenMethod != "open_pin" { services.Res(res).Error(400, "invalid_parameter", "unknown open type method") return } dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // get the object object, found, err := models.FindObjectByObjectID(dbTx, params["id"]) if !found { dbTx.Rollback() services.Res(res).Error(404, "not_found", "object was not found") return } else if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure object belongs to authorizing wallet if object.Wallet.ObjectID != authWalletID { dbTx.Rollback() services.Res(res).Error(401, "unauthorized", "objects: object does not belong to authorizing wallet") return } // set object's open property to true and open_method to `open` clearOpen(&object) object.Open = true object.OpenMethod = models.ObjectOpenDefault // for open_timed, // set 'open_time' field to indicate object open window if body.OpenMethod == "open_timed" { // ensure time field is provided if body.Time == 0 { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "time: open window time is required. use unix time") return } // time must be in the future now := time.Now().UTC() if !now.Before(services.UnixToTime(body.Time).UTC()) { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "time: use a unix time pointing to a period in the future") return } object.OpenMethod = models.ObjectOpenTimed object.OpenTime = body.Time } // for open_pin // open pin sets a pin for used by charge API if body.OpenMethod == "open_pin" { // ensure pin is provided if c.validate.IsEmpty(body.Pin) { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "pin: pin is required") return } // pin must be numeric if !validator.IsNumeric(body.Pin) { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "pin: pin must contain only numeric characters. e.g 4345") return } // pin length must be between 4 - 12 characters if len(body.Pin) < 4 || len(body.Pin) > 12 { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "pin: pin must have a minimum character length of 4 and maximum of 12") return } // hash pin using bcrypt pinHash, err := services.Bcrypt(body.Pin, 10) if err != nil { c.log.Error("unable to hash password. reason: " + err.Error()) services.Res(res).Error(500, "", "server error") return } object.OpenMethod = models.ObjectOpenPin object.OpenPin = pinHash } dbTx.Save(&object).Commit() services.Res(res).Json(object) }
// create a new object by subtracting from a source object func (c *ObjectController) Subtract(res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // TODO: get from access token // authorizing wallet id authWalletID := "55c679145fe09c74ed000001" // parse body var body objectSubtractBody 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 } // object is required if c.validate.IsEmpty(body.Object) { services.Res(res).Error(400, "missing_parameter", "Missing required field: object") return } // amount to subtract is required if body.AmountToSubtract < MinimumObjectUnit { services.Res(res).Error(400, "invalid_parameter", "amount: amount must be equal or greater than the minimum object unit which is 0.00000001") return } dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // get the object object, found, err := models.FindObjectByObjectID(dbTx, body.Object) if !found { dbTx.Rollback() services.Res(res).Error(404, "not_found", "object was not found") return } else if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure object is a valuable type if object.Type == models.ObjectValueless { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "object: object must be a valuabe type (obj_value) ") return } // ensure object belongs to authorizing wallet if object.Wallet.ObjectID != authWalletID { dbTx.Rollback() services.Res(res).Error(401, "unauthorized", "objects: object does not belong to authorizing wallet") return } // ensure object's balance is sufficient if object.Balance < body.AmountToSubtract { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "amount: object's balance is insufficient") return } // if meta is provided, ensure it is not greater than the limit size if !body.InheritMeta && !c.validate.IsEmpty(body.Meta) && len([]byte(body.Meta)) > MaxMetaSize { services.Res(res).Error(400, "invalid_meta_size", fmt.Sprintf("Meta contains too much data. Max size is %d bytes", MaxMetaSize)) return } else { if body.InheritMeta { body.Meta = object.Meta } } // subtract and update object's balance object.Balance = object.Balance - body.AmountToSubtract dbTx.Save(&object) // create new object // generate a pin countryCallCode := config.CurrencyCallCodes[strings.ToUpper(object.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, models.ObjectValue, object.Service, object.Wallet, body.AmountToSubtract, 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 } dbTx.Commit() services.Res(res).Json(newObj) }
// divide an object into two or more equal parts. // maxinum of 100 equal parts is allowed. // object to be splitted must belong to authorizing wallet func (c *ObjectController) Divide(res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // authorizing wallet id // todo: get from access token authWalletID := "55c679145fe09c74ed000001" // parse body var body objectDivideBody 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 } // object is required if c.validate.IsEmpty(body.Object) { services.Res(res).Error(400, "missing_parameter", "Missing required field: object") return } // number of objects must be greater than 1 if body.NumObjects < 2 { services.Res(res).Error(400, "invalid_parameter", "num_objects: must be greater than 1") return } // number of objects must not be greater than 100 if body.NumObjects > 100 { services.Res(res).Error(400, "invalid_parameter", "num_objects: must be greater than 1") return } dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // get the object object, found, err := models.FindObjectByObjectID(dbTx, body.Object) if !found { dbTx.Rollback() services.Res(res).Error(404, "not_found", "object was not found") return } else if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure object is a valuable type if object.Type == models.ObjectValueless { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "object: object must be a valuabe type (obj_value) ") return } // ensure object has enough balance (minimum of 0.000001) if object.Balance < 0.000001 { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "object: object must have a minimum balance of 0.000001") return } // ensure object belongs to authorizing wallet if object.Wallet.ObjectID != authWalletID { dbTx.Rollback() services.Res(res).Error(401, "unauthorized", "object does not belong to authorizing wallet") return } // if meta is provided, ensure it is not greater than the limit size if !body.InheritMeta && !c.validate.IsEmpty(body.Meta) && len([]byte(body.Meta)) > MaxMetaSize { services.Res(res).Error(400, "invalid_meta_size", fmt.Sprintf("Meta contains too much data. Max size is %d bytes", MaxMetaSize)) return } else { if body.InheritMeta { body.Meta = object.Meta } } // calculate new balance per object newBalance := object.Balance / float64(body.NumObjects) // delete object dbTx.Delete(&object) // create new objects newObjects := []models.Object{} for i := 0; i < body.NumObjects; i++ { // generate a pin countryCallCode := config.CurrencyCallCodes[strings.ToUpper(object.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, models.ObjectValue, object.Service, object.Wallet, newBalance, 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 } newObjects = append(newObjects, newObj) } dbTx.Commit() services.Res(res).Json(newObjects) }
// merge two or more objects. // Only a max of 100 identitcal objects can be merged. // All objects to be merged must exists. // Only similar objects can be merged. // Meta is not retained. Optional "meta" parameter can be // provided as new meta for the resulting object // TODO: Needs wallet authorization with scode "obj_merge" func (c *ObjectController) Merge(res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // authorizing wallet id // TODO: get this from the access token authWalletID := "55c679145fe09c74ed000001" // parse body var body objectMergeBody 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 } // objects field is required if body.Objects == nil { services.Res(res).Error(400, "missing_parameter", "Missing required field: objects") return } // objects field must contain at least two objects if len(body.Objects) < 2 { services.Res(res).Error(400, "invalid_parameter", "objects: minimum of two objects required") return } // objects field must not contain more than 100 objects if len(body.Objects) > 100 { services.Res(res).Error(400, "invalid_parameter", "objects: cannot merge more than 100 objects in a request") return } // ensure objects contain no duplicates if services.StringSliceHasDuplicates(body.Objects) { services.Res(res).Error(400, "invalid_parameter", "objects: must not contain duplicate objects") 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).Error(400, "invalid_meta_size", fmt.Sprintf("Meta contains too much data. Max size is %d bytes", MaxMetaSize)) return } // get db transaction object dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // find all objects objectsFound, err := models.FindAllObjectsByObjectID(dbTx, body.Objects) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure all objects where found if len(objectsFound) != len(body.Objects) { dbTx.Rollback() services.Res(res).Error(400, "unknown_merge_objects", "one or more objects does not exists") return } totalBalance := 0.0 firstObj := objectsFound[0] checkObjName := firstObj.Service.Identity.ObjectName for _, object := range objectsFound { // ensure all objects are valuable and also ensure object's if object.Type == models.ObjectValueless { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "objects: only valuable objects (object_value) can be merged") return } // wallet id match the authorizing wallet id if object.Wallet.ObjectID != authWalletID { dbTx.Rollback() services.Res(res).Error(401, "unauthorized", "objects: one or more objects belongs to a different wallet") return } // ensure all objects are similar by their name / same issuer. // this also ensures all objects have the same base currency if checkObjName != object.Service.Identity.ObjectName { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "objects: only similar (by name) objects can be merged") return } // updated total balance totalBalance += object.Balance // delete object dbTx.Delete(&object) } // create a new object // generate a pin countryCallCode := config.CurrencyCallCodes[strings.ToUpper(firstObj.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, models.ObjectValue, firstObj.Service, firstObj.Wallet, totalBalance, 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 } dbTx.Commit() services.Res(res).Json(newObj) }
// list object // supports // - pagination using 'page' query. Use per_page to set the number of results per page. max is 100 // - filters: filter_type, filter_service, filter_open, filter_open_method, filter_gte_date_created // filter_lte_date_created // - sorting: sort_balance, sort_date_created func (c *WalletController) List(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { // TODO: get from access token // authorizing wallet id authWalletID := "55c679145fe09c74ed000001" dbCon := db.GetPostgresHandle() // get wallet wallet, found, err := models.FindWalletByObjectID(dbCon, params["id"]) if !found { services.Res(res).Error(404, "not_found", "wallet not found") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure wallet matches authorizing wallet if wallet.ObjectID != authWalletID { services.Res(res).Error(401, "unauthorized", "client does not have permission to access wallet") return } query := req.URL.Query() qPage := query.Get("page") if c.validate.IsEmpty(qPage) { qPage = "0" } else if !validator.IsNumeric(qPage) { services.Res(res).Error(400, "invalid_parameter", "page query value must be numeric") return } q := make(map[string]interface{}) q["wallet_id"] = wallet.ID order := "id asc" limitPerPage := int64(2) offset := int64(0) currentPage, err := strconv.ParseInt(qPage, 0, 64) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // set limit per page if provided in query qPerPage := query.Get("per_page") if !c.validate.IsEmpty(qPerPage) { if validator.IsNumeric(qPerPage) { qPerPage, _ := strconv.ParseInt(qPerPage, 0, 64) if qPerPage > 100 { qPerPage = 100 } else if qPerPage <= 0 { qPerPage = limitPerPage } limitPerPage = qPerPage } } // set current page default and calculate offset if currentPage <= 1 { currentPage = 1 offset = 0 } else { offset = (int64(limitPerPage) * currentPage) - int64(limitPerPage) } // apply type filter if included in query filterType := query.Get("filter_type") if !c.validate.IsEmpty(filterType) && services.StringInStringSlice([]string{"obj_value", "obj_valueless"}, filterType) { q["type"] = filterType } // apply service filter if included in query filterService := query.Get("filter_service") if !c.validate.IsEmpty(filterService) { // find service service, found, err := models.FindServiceByObjectID(db.GetPostgresHandle(), filterService) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } if found { q["service_id"] = service.ID } } // apply open filter if included in query filterOpen := query.Get("filter_open") if !c.validate.IsEmpty(filterOpen) && services.StringInStringSlice([]string{"true", "false"}, filterOpen) { q["open"] = filterOpen } // apply open_method filter if included in query filterOpenMethod := query.Get("filter_open_method") if !c.validate.IsEmpty(filterOpenMethod) && services.StringInStringSlice([]string{"open", "open_timed", "open_pin"}, filterOpenMethod) { q["open_method"] = filterOpenMethod } // apply filter_gte_date_created filter if included in query filterGTEDateCreated := query.Get("filter_gte_date_created") if !c.validate.IsEmpty(filterGTEDateCreated) { if validator.IsNumeric(filterGTEDateCreated) { ts, _ := strconv.ParseInt(filterGTEDateCreated, 0, 64) dbCon = dbCon.Where("created_at >= ?", services.UnixToTime(ts).UTC().Format(time.RFC3339Nano)) } } // apply filter_lte_date_created filter if included in query filterLTEDateCreated := query.Get("filter_lte_date_created") if !c.validate.IsEmpty(filterLTEDateCreated) { if validator.IsNumeric(filterLTEDateCreated) { ts, _ := strconv.ParseInt(filterLTEDateCreated, 0, 64) dbCon = dbCon.Where("created_at <= ?", services.UnixToTime(ts).UTC().Format(time.RFC3339Nano)) } } // the below connection is used for sorting/ordering var dbConSort = dbCon // apply sort_balance sort if included sortBalance := query.Get("sort_balance") if !c.validate.IsEmpty(sortBalance) { orderVal := "asc" if sortBalance == "-1" { orderVal = "desc" } dbConSort = dbCon.Order("objects.balance " + orderVal) } // apply ort_date_created sort if included sortDateCreated := query.Get("sort_date_created") if !c.validate.IsEmpty(sortDateCreated) { orderVal := "asc" if sortDateCreated == "-1" { orderVal = "desc" } dbConSort = dbConSort.Order("objects.created_at " + orderVal) } // find objects associated with wallet objects := []models.Object{} var objectsCount int64 // count number of objects. I didnt use dbConSort as count will throw an error dbCon.Model(models.Object{}).Where(q).Count(&objectsCount) // set the original db connection to the sort connection dbCon = dbConSort // calculate number of pages numPages := services.Round(float64(objectsCount) / float64(limitPerPage)) // fetch the objects dbCon.Where(q).Preload("Service.Identity").Preload("Wallet.Identity").Limit(limitPerPage).Offset(offset).Order(order).Find(&objects) // prepare response respObj, _ := services.StructToJsonToSlice(objects) if len(respObj) == 0 { respObj = []map[string]interface{}{} } services.Res(res).Json(map[string]interface{}{ "results": respObj, "_metadata": map[string]interface{}{ "total_count": objectsCount, "per_page": limitPerPage, "page_count": numPages, "page": currentPage, }, }) }
// enable a service to issuer status func (c *ServiceController) EnableIssuer(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { // parse request body var body enableIssuerBody if err := c.ParseJsonBody(req, &body); err != nil { services.Res(res).Error(400, "invalid_client", "request body is invalid or malformed. Expects valid json body") return } // service id is required if c.validate.IsEmpty(body.ServiceID) { services.Res(res).Error(400, "missing_parameter", "Missing required field: service_id") return } // ensure service exists service, found, err := models.FindServiceByObjectID(db.GetPostgresHandle(), body.ServiceID) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } else if !found { services.Res(res).Error(404, "not_found", "service was not found") return } // object name is required if c.validate.IsEmpty(body.ObjectName) { services.Res(res).Error(400, "missing_parameter", "Missing required field: object_name") return } // ensure no other service has used the object name identity, found, err := models.FindIdentityByObjectName(db.GetPostgresHandle(), body.ObjectName) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } else if found && identity.ObjectID != service.Identity.ObjectID { services.Res(res).Error(400, "invalid_object_name", "object name is not available, try a unique name") return } // base currency is required if validator.IsNull(body.BaseCurrency) { services.Res(res).Error(400, "missing_parameter", "Missing required field: base_currency") return } // base currency must be supported if !services.StringInStringSlice(config.SupportedBaseCurrencies, body.BaseCurrency) { services.Res(res).Error(400, "invalid_base_currency", "base currency is unknown") return } // set issuer to true service.Identity.Issuer = true service.Identity.ObjectName = strings.ToLower(body.ObjectName) service.Identity.BaseCurrency = body.BaseCurrency db.GetPostgresHandle().Save(&service) respObj, _ := services.StructToJsonToMap(service) respObj["identity"].(map[string]interface{})["soul_balance"] = service.Identity.SoulBalance services.Res(res).Json(respObj) }
// create a wallet func (c *WalletController) Create(res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { // parse body var body walletCreateBody 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 } // identity id is required if validator.IsNull(body.IdentityId) { services.Res(res).Error(400, "missing_parameter", "Missing required field: identity_id") return } // handle is required if validator.IsNull(body.Handle) { services.Res(res).Error(400, "missing_parameter", "Missing required field: handle") return } // password is required if validator.IsNull(body.Password) { services.Res(res).Error(400, "missing_parameter", "Missing required field: password") return } // identity id must exist identity, found, err := models.FindIdentityByObjectID(db.GetPostgresHandle(), body.IdentityId) if !found { services.Res(res).Error(404, "invalid_identity", "identity_id is unknown") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // handle must be unique across wallets _, found, err = models.FindWalletByHandle(db.GetPostgresHandle(), body.Handle) if found { services.Res(res).Error(400, "handle_registered", "handle has been registered to another wallet") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // password length must be 6 characters if len(body.Password) < 6 { services.Res(res).Error(400, "invalid_password", "password is too short. minimum length is 6 characters") return } // securely hash password hashedPassword, err := services.Bcrypt(body.Password, 10) if err != nil { c.log.Error("unable to hash password. reason: " + err.Error()) services.Res(res).Error(500, "", "server error") return } else { body.Password = hashedPassword } // create wallet object newWallet := models.Wallet{ ObjectID: bson.NewObjectId().Hex(), Identity: identity, Handle: body.Handle, Password: body.Password, } // create wallet err = models.CreateWallet(db.GetPostgresHandle(), &newWallet) if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } respObj, _ := services.StructToJsonToMap(newWallet) services.Res(res).Json(respObj) }
// 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) }
// get counts and other numerical information about the state of a wallet // e.g object balance and count etc func (c *WalletController) Numbers(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, db *services.DB) { // TODO: get from access token // authorizing wallet id authWalletID := "55c679145fe09c74ed000001" dbCon := db.GetPostgresHandle() resp := map[string]interface{}{} query := req.URL.Query() count := int64(0) // predefine default query values query.Set("object_count", "true") query.Set("distinct_object_count", "true") query.Set("valuable_object_count", "true") query.Set("valueless_object_count", "true") query.Set("valueable_object_balance", "true") // get wallet wallet, found, err := models.FindWalletByObjectID(dbCon, params["id"]) if !found { services.Res(res).Error(404, "not_found", "wallet not found") return } else if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure wallet matches authorizing wallet if wallet.ObjectID != authWalletID { services.Res(res).Error(401, "unauthorized", "client does not have permission to access wallet") return } // object_count objectCountField := query.Get("object_count") if !c.validate.IsEmpty(objectCountField) && services.StringInStringSlice([]string{"true", "false"}, objectCountField) { if objectCountField == "true" { q := map[string]interface{}{ "wallet_id": wallet.ID, } dbCon.Model(models.Object{}).Where(q).Count(&count) resp["object_count"] = count count = 0 } } // distinct_object_count distinctObjectCountField := query.Get("distinct_object_count") if !c.validate.IsEmpty(distinctObjectCountField) && services.StringInStringSlice([]string{"true", "false"}, distinctObjectCountField) { if distinctObjectCountField == "true" { row, err := dbCon.Raw("SELECT COUNT(*) FROM (SELECT DISTINCT service_id FROM objects WHERE wallet_id = ?) AS distinct_object_count;", wallet.ID).Rows() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } if row.Next() { row.Scan(&count) } resp["distinct_object_count"] = count count = 0 } } // valuable_object_count valuableObjectCountField := query.Get("valuable_object_count") if !c.validate.IsEmpty(distinctObjectCountField) && services.StringInStringSlice([]string{"true", "false"}, valuableObjectCountField) { if valuableObjectCountField == "true" { q := map[string]interface{}{ "wallet_id": wallet.ID, "type": models.ObjectValue, } dbCon.Model(models.Object{}).Where(q).Count(&count) resp["valuable_object_count"] = count count = 0 } } // valueless_object_count valuelessObjectCountField := query.Get("valueless_object_count") if !c.validate.IsEmpty(valuelessObjectCountField) && services.StringInStringSlice([]string{"true", "false"}, valuelessObjectCountField) { if valuelessObjectCountField == "true" { q := map[string]interface{}{ "wallet_id": wallet.ID, "type": models.ObjectValueless, } dbCon.Model(models.Object{}).Where(q).Count(&count) resp["valueless_object_count"] = count count = 0 } } // valueable_object_balance valuableObjectBalanceField := query.Get("valueable_object_balance") if !c.validate.IsEmpty(distinctObjectCountField) && services.StringInStringSlice([]string{"true", "false"}, valuableObjectBalanceField) { if valuableObjectBalanceField == "true" { row, err := dbCon.Raw("SELECT SUM(balance) AS total_balance FROM objects WHERE wallet_id = ? AND type = ?;", wallet.ID, models.ObjectValue).Rows() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } if row.Next() { row.Scan(&count) } resp["valueable_object_balance"] = count count = 0 } } // opened_object_count openedObjectCountField := query.Get("opened_object_count") if !c.validate.IsEmpty(openedObjectCountField) && services.StringInStringSlice([]string{"true", "false"}, openedObjectCountField) { if openedObjectCountField == "true" { q := map[string]interface{}{ "wallet_id": wallet.ID, "open": true, } dbCon.Model(models.Object{}).Where(q).Count(&count) resp["opened_object_count"] = count count = 0 } } // locked_object_count lockedObjectCountField := query.Get("locked_object_count") if !c.validate.IsEmpty(lockedObjectCountField) && services.StringInStringSlice([]string{"true", "false"}, lockedObjectCountField) { if lockedObjectCountField == "true" { q := map[string]interface{}{ "wallet_id": wallet.ID, "open": false, } dbCon.Model(models.Object{}).Where(q).Count(&count) resp["locked_object_count"] = count count = 0 } } // opened_timed_object_count openedTimedObjectCountField := query.Get("opened_timed_object_count") if !c.validate.IsEmpty(openedObjectCountField) && services.StringInStringSlice([]string{"true", "false"}, openedTimedObjectCountField) { if openedTimedObjectCountField == "true" { q := map[string]interface{}{ "wallet_id": wallet.ID, "open": true, "open_method": models.ObjectOpenTimed, } dbCon.Model(models.Object{}).Where(q).Count(&count) resp["opened_timed_object_count"] = count count = 0 } } // opened_pin_object_count openedPinObjectCountField := query.Get("opened_pin_object_count") if !c.validate.IsEmpty(openedPinObjectCountField) && services.StringInStringSlice([]string{"true", "false"}, openedPinObjectCountField) { if openedPinObjectCountField == "true" { q := map[string]interface{}{ "wallet_id": wallet.ID, "open": true, "open_method": models.ObjectOpenPin, } dbCon.Model(models.Object{}).Where(q).Count(&count) resp["opened_pin_object_count"] = count count = 0 } } services.Res(res).Json(resp) }
// ensures authorizaion header is a `Bearer` scheme func MustBeBearer(res http.ResponseWriter, arc services.AuxRequestContext, log *config.CustomLog) { authorization := strings.ToLower(arc.Header.Get("Authorization")) if !services.StringStartsWith(authorization, "bearer") { services.Res(res).Error(401, "invalid_request", "authorization scheme must be Bearer") } }
// 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) }