diff --git a/apps/README.md b/apps/README.md index 20cc02072a153e156482705352901880a938f37d..e1874dad259d2e0b77d9a248284716d4dc8cd20c 100644 --- a/apps/README.md +++ b/apps/README.md @@ -89,7 +89,7 @@ Moved to stock caaa642c25d96a5d8f14cf031a898fed $ cd ../cement-cli $ node index.js bid ? Select asset 19e2f7a99288dfc9a2902e71bfc9dfe6 (Stock, type: 1002, status: stocked) -? Choose an ammount of slag 34 +? Choose an amount of slag 34 ? Provide the offered price 3 Bid for slag stock 19e2f7a99288dfc9a2902e71bfc9dfe6 @@ -100,8 +100,29 @@ $ node index.js bid-accept 0LWNvbXBhbnkxLmNvbSxPPWNlbWVudC1jb21wYW55MS5jb20sTD1SYWxlaWdoLFNUPU5vcnRoIENhcm9saW5hLEM9VVM= (quantity: 34, price: 3) Bid from cement-company1-com:eDUwOTo6Q049dXNlcixPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2EuY2VtZW50LWNvbXBhbnkxLmNvbSxPPWNlbWVudC1jb21wYW55MS5jb20sTD1SYWxlaWdoLFNUPU5vcnRoIENhcm9saW5hLEM9VVM= accepted for asset 19e2f7a99288dfc9a2902e71bfc9dfe6 +$ node index.js discard-stock +? Select asset 7bbed24d305800c2f2e61b7f14539249 (Stock, type: 1002, status: stocked) +? Choose an amount of slag (max: 7297 kg): 729 +Discarded stock 15fb63d1d6071f9ee101bc41e5b4c75f (new stock e511a4dbb32669282e14838ec012e2e0) + $ cd ../pubadmin-cli $ node index.js stats +┌─────────────┬────────────┬─────────────────┬────────────────┬───────────────────────┐ +│ │ Total slag │ Reused │ Discarded │ Price (€/t) │ +│ │ t │ t [%] │ t [%] │ Avg [Min, Max] │ +├─────────────┼────────────┼─────────────────┼────────────────┼───────────────────────┤ +│ sidenor.com │ 17.375 │ 3.763 [21.66 %] │ 1.302 [7.49 %] │ 18.13 [49.74, 522.61] │ +└─────────────┴────────────┴─────────────────┴────────────────┴───────────────────────┘ + +┌─────────────────────┬────────────┬─────────────────────────┐ +│ │ Total slag │ Price (€/t) │ +│ │ t │ Avg [Min, Max] │ +├─────────────────────┼────────────┼─────────────────────────┤ +│ cement-company1.com │ 0.667 │ 241.38 [121.79, 522.61] │ +├─────────────────────┼────────────┼─────────────────────────┤ +│ cement-company2.com │ 3.096 │ 49.74 [49.74, 49.74] │ +└─────────────────────┴────────────┴─────────────────────────┘ + # Or to generate/simulate a complete cycle: $ cd ../batch-sim diff --git a/apps/batch-sim/index.js b/apps/batch-sim/index.js index 714f3a283f815c8dd1fb57c5bf1f67ec07819bec..921fd57eeda9ee1701436b8164418fb892340a94 100644 --- a/apps/batch-sim/index.js +++ b/apps/batch-sim/index.js @@ -9,7 +9,8 @@ const { editComposition, sendToCone, sendToStock, - acceptBid + acceptBid, + discardStock } = require("@hypercog/sidenor-cli") const { bidAsset } = require("@hypercog/cement-cli") @@ -42,23 +43,27 @@ const simulateCycle = async (arg1, cmd) => { //batchId = await createAssetAndSendToCone(apiClient, "simulated asset 2") //batchId = await createAssetAndSendToCone(apiClient, "simulated asset 3") - const selectedStock = await sendToStock(apiClient, { assetId: batchId }) + const mergedStock = await sendToStock(apiClient, { assetId: batchId }) + const selectedStockId = await discardStock(apiClient, { + assetId: mergedStock.id + }) + const selectedStock = await apiClient.getAsset(selectedStockId) await signInAs(apiClient, otherArgs.cement1User, password) await bidAsset(apiClient, { - assetId: selectedStock.id, + assetId: selectedStockId, price: faker.datatype.number({ min: 50, max: 150 }) }) await signInAs(apiClient, otherArgs.cement2User, password) await bidAsset(apiClient, { - assetId: selectedStock.id, + assetId: selectedStockId, quantity: selectedStock.quantity - 500, price: faker.datatype.number({ min: 25, max: 200 }) }) await signInAs(apiClient, otherArgs.sidenorUser, password) - await acceptBid(apiClient, { assetId: selectedStock.id }) + await acceptBid(apiClient, { assetId: selectedStockId }) } const program = createCLIApp() diff --git a/apps/cement-cli/lib.js b/apps/cement-cli/lib.js index fdd44dc4c76d19d0a5417aefe043526ff88f116c..fc54b70c2aad9bb1d1dbb0fc7df8b60f9bef4ec8 100644 --- a/apps/cement-cli/lib.js +++ b/apps/cement-cli/lib.js @@ -19,7 +19,7 @@ const bidAsset = async ( { type: "number", name: "inputQuantity", - message: `Choose an ammount of slag (max: ${assetToBid.quantity} ${assetToBid.units}):`, + message: `Choose an amount of slag to bid for (max: ${assetToBid.quantity} ${assetToBid.units}):`, default: Math.floor(assetToBid.quantity * 0.1) // Default: 10% } ]) diff --git a/apps/sidenor-cli/lib.js b/apps/sidenor-cli/lib.js index a3e730baea4ddcc4bccc716f48ecf958f6cf7ad2..ea6eb6244a7966b047449f3622d0019e18de302d 100644 --- a/apps/sidenor-cli/lib.js +++ b/apps/sidenor-cli/lib.js @@ -234,37 +234,20 @@ const discardStock = async (apiClient, { assetId: paramAssetId, quantity }) => { { type: "number", name: "inputQuantity", - message: `Choose an ammount of slag (max: ${assetToDiscard.quantity} ${assetToDiscard.units}):`, + message: `Choose an amount of slag to discard (max: ${assetToDiscard.quantity} ${assetToDiscard.units}):`, default: Math.floor(assetToDiscard.quantity * 0.1) // Default: 10% } ]) chosenQuantity = inputQuantity } - const newAssets = await apiClient.splitAsset(assetId, 2, { - archiveOld: true, - bidirectional: true, - base: { - type: assetToDiscard.type, - fields: assetToDiscard.fields - } - }) - - await apiClient.modifyAsset({ - id: newAssets[0], - quantity: assetToDiscard.quantity - chosenQuantity, - units: assetToDiscard.units - }) - - await apiClient.modifyAsset({ - id: newAssets[1], - quantity: chosenQuantity, - units: assetToDiscard.units - }) - - await apiClient.deleteAsset(newAssets[1]) + const [newStock, discardedBatch] = await apiClient.deleteAsset( + assetId, + chosenQuantity + ) - console.log(`Discarded stock ${newAssets[1]} (new stock ${newAssets[0]})`) + console.log(`Discarded stock ${discardedBatch} (new stock ${newStock})`) + return newStock } // To be reused as a library in @hypercog/batch-sim diff --git a/apps/utils/client.js b/apps/utils/client.js index c4fb3b83caefead13cf86638226958850165939e..6c93704307dd6f3df52092ec9c2c63ccecc0b4f0 100644 --- a/apps/utils/client.js +++ b/apps/utils/client.js @@ -38,6 +38,25 @@ class HypercogApiClient extends TraceblockApiClient { body: { id, bidder } }) } + + /** + * Archives an asset (overrides original definition). + * + * @param {string} id Asset identifier. + * @param {number} quantity Magnitude measured in the units specified in the associated asset definition. + */ + async deleteAsset(id, quantity) { + const { payload } = await this._invoke("asset-archive", { + body: { id, quantity } + }) + return payload + } + + Bid(id, bidder) { + return this._invoke("asset-archive", { + body: { id, bidder } + }) + } } module.exports = HypercogApiClient diff --git a/chaincode/controller/discard/acl.go b/chaincode/controller/discard/acl.go new file mode 100644 index 0000000000000000000000000000000000000000..1e9e4b470b6ba9b84acf9dd259372c3aed4286fd --- /dev/null +++ b/chaincode/controller/discard/acl.go @@ -0,0 +1,38 @@ +/** + * acl.go + * + * COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022. + */ + +package discard + +import ( + "git.code.tecnalia.com/ledgerbuilder/sdk/core/api" + "git.code.tecnalia.com/ledgerbuilder/sdk/core/fabric/protos" + "git.code.tecnalia.com/ledgerbuilder/sdk/shared" + + "git.code.tecnalia.com/traceblock/sdk/controller/utils" +) + +func (c DiscardController) CheckACL(validations []utils.OwnershipACL, trigger shared.TriggerFunction) shared.TriggerFunction { + return func(stub shared.LedgerBuildrStubInterface, requestAsset shared.LedgerBuildrAsset) protos.Response { + const fnName = "ACL" + + storedAsset, err := c.readAsset(stub, requestAsset) + if err != nil { + return api.NewApiResponsePtr(fnName, err, nil).SendResponse() + } + + for _, validation := range validations { + if err := validation(storedAsset.Owner, stub); err != nil { + return api.NewApiResponsePtr(fnName, err, nil).SendResponse() + } + } + + return trigger(stub, requestAsset) + } +} + +func (c DiscardController) OnlySameOrgAndRole(trigger shared.TriggerFunction) shared.TriggerFunction { + return c.CheckACL([]utils.OwnershipACL{utils.OwnedBySameOrg, utils.OwnedBySameRoleOrAdmin}, trigger) +} \ No newline at end of file diff --git a/chaincode/controller/discard/controller.go b/chaincode/controller/discard/controller.go new file mode 100644 index 0000000000000000000000000000000000000000..d8e24d709fb639c37bbe7ceeca791196cc121c33 --- /dev/null +++ b/chaincode/controller/discard/controller.go @@ -0,0 +1,159 @@ +/** + * controller.go + * + * COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022. + */ + +package discard + +import ( + "errors" + + "git.code.tecnalia.com/ledgerbuilder/sdk/core/api" + "git.code.tecnalia.com/ledgerbuilder/sdk/core/controller" + "git.code.tecnalia.com/ledgerbuilder/sdk/core/fabric/protos" + "git.code.tecnalia.com/ledgerbuilder/sdk/shared" + + "git.code.tecnalia.com/blockchain/hypercog/controller/stats" + + errs "git.code.tecnalia.com/traceblock/sdk/constants" + "git.code.tecnalia.com/traceblock/sdk/controller/base" + "git.code.tecnalia.com/traceblock/sdk/controller/split" + "git.code.tecnalia.com/traceblock/sdk/model" +) + +type DiscardController struct { + base.TraceblockBaseController +} + +var ( + errInvalidInputData = errors.New("failed to read model") + errInvalidDiscardQuantity = errors.New("quantity to discard is greater than the available quantity") +) + +// constructor like function +func NewDiscardController() *DiscardController { + ctl := new(DiscardController) + ctl.LowLevelController = controller.NewLowLevelController() + ctl.SetDataModelClosure(ctl.modelClosure) + return ctl +} + +func (c DiscardController) modelClosure(stub shared.LedgerBuildrStubInterface) shared.LedgerBuildrAsset { + return NewDiscardParams() +} + +func (c DiscardController) readTraceableAsset(stub shared.LedgerBuildrStubInterface, assetId string) (*model.TraceableAsset, error) { + readedModel, err := c.ReadAbstractAssetAndReturn(stub, assetId, model.NewTraceableAsset()) + if err != nil { + return nil, err + } + + // asset data successfully readed + asset, ok := readedModel.(*model.TraceableAsset) + if !ok { + return nil, errs.NotExistingAsset + } + + return asset, nil +} + +func (c DiscardController) readAsset(stub shared.LedgerBuildrStubInterface, requestAsset shared.LedgerBuildrAsset) (*model.TraceableAsset, error) { + return c.readTraceableAsset(stub, requestAsset.GetID()) +} + + +// HyperCOG specific discard operation +func (c DiscardController) _discardAsset(stub shared.LedgerBuildrStubInterface, params *DiscardParams, discardableAsset *model.TraceableAsset) (*protos.Response, error) { + if discardableAsset.Quantity < params.Quantity { + return nil, errInvalidDiscardQuantity + } + + var discardedStock *model.TraceableAsset + generatedIds := make([]string, 2) + + if params.Quantity == discardableAsset.Quantity { + discardedStock = discardableAsset + } else { + // discardableAsset.Quantity > discardParams.Quantity + splitParams := new(split.SplitParams) + splitParams.SetID(discardableAsset.GetID()) + splitParams.SplitConfig.ArchiveOld = true + splitParams.SplitConfig.Bidirectional = true + splitParams.SplitConfig.ChildCount = 2 + splitParams.SplitConfig.BaseAsset.AssetType = discardableAsset.AssetType + splitParams.SplitConfig.BaseAsset.ArbitraryDataFields = discardableAsset.ArbitraryDataFields + + subAssets, err := split.SplitAsset(c.TraceblockBaseController, stub, discardableAsset, *splitParams) + if err != nil { + return nil, err + } + + for i, c := range subAssets { + generatedIds[i] = c.GetID() + } + + newStock := subAssets[0] + newStock.Quantity = discardableAsset.Quantity - params.Quantity + newStock.Units = discardableAsset.Units + + respNewStock:= c.SaveAbstractAsset(stub, &newStock) + if respNewStock.Status != shared.OK { + return &respNewStock, nil + } + + discardedStock = &subAssets[1] + + discardedStock.Quantity = params.Quantity + discardedStock.Units = discardableAsset.Units + } + + discardedStock.ArbitraryDataFields["status"] = "discarded" + + err := stats.RegisterDiscard(stub, discardedStock.Quantity, discardedStock.Units) + if err != nil { + return nil, err + } + + respDiscarted := c.SaveAbstractAsset(stub, discardedStock) + if respDiscarted.Status != shared.OK { + return &respDiscarted, nil + } + + ret := api.NewAPIGenericResponsePtr("DiscardController:Discard", nil, generatedIds).SendResponse() + return &ret, nil +} + +func (c DiscardController) _archiveAsset(stub shared.LedgerBuildrStubInterface, params shared.LedgerBuildrAsset) (*protos.Response, error) { + discardParams, ok := params.(*DiscardParams) + if discardParams == nil || !ok { + //failed when casting/fetching input data + return nil, errInvalidInputData + } + + discardableAsset, err := c.readAsset(stub, discardParams) + if err != nil { + return nil, err + } + + status, ok := discardableAsset.Get("status") + if status != "stocked" || status == "stocked" && discardParams.Quantity == 0 { + // Done in MarkAssetAsDeleted middleware in normal operation + discardableAsset.MarkModification(stub) + discardableAsset.MarkAsDeleted() + + responseArchive := c.DeleteAbstractAsset(stub, discardableAsset) + return &responseArchive, nil + } + + // HyperCOG specific discard operation + return c._discardAsset(stub, discardParams, discardableAsset) +} + +func (c DiscardController) Discard(stub shared.LedgerBuildrStubInterface, params shared.LedgerBuildrAsset) protos.Response { + ret, err := c._archiveAsset(stub, params) + if err != nil { + return api.NewApiResponsePtr("DiscardController:Discard", err, nil).SendResponse() + } + return *ret +} \ No newline at end of file diff --git a/chaincode/controller/discard/input.go b/chaincode/controller/discard/input.go new file mode 100644 index 0000000000000000000000000000000000000000..4a03256bb3ed80aef64ca1aa6cc8ab2bdbc508fd --- /dev/null +++ b/chaincode/controller/discard/input.go @@ -0,0 +1,68 @@ +/** + * input.go + * + * COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022. + */ + +package discard + +import ( + "git.code.tecnalia.com/ledgerbuilder/sdk/core/model/io" + "git.code.tecnalia.com/ledgerbuilder/sdk/shared" + errs "git.code.tecnalia.com/traceblock/sdk/constants" +) + +type DiscardParams struct { + shared.LedgerBuildrAsset + AssetId string `json:"id"` + Quantity uint32 `json:"quantity,omitempty"` +} + +func (p *DiscardParams) GetID() string { + return p.AssetId +} + +func (p *DiscardParams) Get(key string) (interface{}, bool) { + return nil, false +} + +func (p *DiscardParams) SetID(id string) { + p.AssetId = id +} + +func (p *DiscardParams) StubBytes() []byte { + return []byte{} +} + +func (p *DiscardParams) LedgerBytes() []byte { + return nil +} + +func (p *DiscardParams) ReadValidation() bool { + return false +} + +// set write validations to false to avoid ledger persistency +func (p *DiscardParams) WriteValidation() bool { + return false +} + +func (p *DiscardParams) StubReader(stub shared.LedgerBuildrStubInterface) (shared.LedgerBuildrAsset, error) { + if stub != nil { + parseErr := io.NewJSONSerializer().LoadFromByteArray(&p, stub.GetPayload()) + return p, parseErr + } + return nil, errs.StubReader +} + +func (p *DiscardParams) LedgerReader(data []byte) (shared.LedgerBuildrAsset, error) { + if data != nil { + parseErr := io.NewJSONSerializer().LoadFromByteArray(&p, data) + return p, parseErr + } + return nil, errs.LedgerReader +} + +func NewDiscardParams() shared.LedgerBuildrAsset { + return new(DiscardParams) +} diff --git a/chaincode/controller/operations.go b/chaincode/controller/operations.go index dd68be5bf7c02436c526ebf3a3cbbea0ca96a4eb..e270c4f3e168e90e2cd6d28fae58b908b7c38f01 100644 --- a/chaincode/controller/operations.go +++ b/chaincode/controller/operations.go @@ -21,6 +21,7 @@ import ( "git.code.tecnalia.com/traceblock/sdk/middleware" "git.code.tecnalia.com/blockchain/hypercog/controller/bid" + "git.code.tecnalia.com/blockchain/hypercog/controller/discard" "git.code.tecnalia.com/blockchain/hypercog/controller/stats" m2 "git.code.tecnalia.com/blockchain/hypercog/middleware" ) @@ -40,24 +41,22 @@ var ( // bid controller bidController *bid.BidController bidDecisionController *bid.BidDecisionController + + discardController *discard.DiscardController ) func init() { assetController = asset.NewTraceableAssetController() - // for debug/testing functions - testController = controller.NewHelperController() - // for ui related helper calls - uiController = controller.NewUIController() - // transfer controller transferController = transfer.NewTransferAssetController() - // asset split controller splitController = split.NewSplitController() - // asset join controller joinController = join.NewJoinController() - // qr controller qrController = qr.NewQRController() - // barcode controller barcodeController = barcode.NewBarcodeController() + discardController = discard.NewDiscardController() + // for ui related helper calls + uiController = controller.NewUIController() + // for debug/testing functions + testController = controller.NewHelperController() // bid controllers bidController = bid.NewBidController() bidDecisionController = bid.NewBidDecisionController() @@ -113,14 +112,15 @@ func ContextOperations(m shared.AbstractChaincodeOperationManager) error { nil, assetController.OnlySameOrgAndRole(assetController.UpdateAsset), ). - AddOperation( + AddOperationWithoutDefaults( "asset-archive", shared.WRITE_OP, - []shared.MiddlewareInterface{ - middleware.MarkAssetAsDeleted, - }, nil, - assetController.OnlySameOrgAndRole(assetController.DeleteAbstractAsset), + discardController.StubReader, + discardController.LedgerReader, + nil, + nil, + discardController.OnlySameOrgAndRole(discardController.Discard), ). AddOperationWithoutDefaults( "asset-transfer", diff --git a/chaincode/controller/stats/stub.go b/chaincode/controller/stats/stub.go index 26359f5f61993d04129e6b97e6160bb1c660dfd6..66066ad42567f667501f4c701f93a2527b07e815 100644 --- a/chaincode/controller/stats/stub.go +++ b/chaincode/controller/stats/stub.go @@ -220,6 +220,23 @@ func RegisterSlag(stub shared.LedgerBuildrStubInterface, quantity uint32, units return nil } + +func RegisterDiscard(stub shared.LedgerBuildrStubInterface, quantity uint32, units string) (error) { + orgStats, err := retrieveExistingStats(stub) + if err != nil { + return err + } + + orgStats.RegisterDiscard(quantity, units) + + err = upgradeSteelStats(stub, orgStats) + if err != nil { + return err + } + + return nil +} + func RegisterSale(stub shared.LedgerBuildrStubInterface, acceptedBid *model.Bid, units string) (error) { senderOrgStats, err := retrieveExistingStats(stub) if err != nil { diff --git a/chaincode/model/stats/steel.go b/chaincode/model/stats/steel.go index 14020c08d2860ce5ad2a1a844b4959b0e54a816a..5293d659bed2e060039423ccb3a71d9a798509ec 100644 --- a/chaincode/model/stats/steel.go +++ b/chaincode/model/stats/steel.go @@ -28,6 +28,10 @@ func (raw *RawSteelOrgStats) RegisterSlag(quantity uint32, units string) { raw.TotalSlag += toTons(quantity, units) } +func (raw *RawSteelOrgStats) RegisterDiscard(quantity uint32, units string) { + raw.SlagDiscarded += toTons(quantity, units) +} + func (raw *RawSteelOrgStats) UpgradeSale(quantity uint32, price float32, units string) { soldBatch := toTons(quantity, units) raw.SlagReused += soldBatch