github.com/s7techlab/cckit@v0.10.5/state/README.md (about) 1 # Working with Hyperledger Fabric chaincode state with CCKit 2 3 Chaincode is a domain specific program which relates to specific business process. It programmatically accesses 4 two distinct pieces of the ledger – a blockchain, which immutably records the history of all transactions, and a world state 5 that holds a cache of the current value of these states. The job of a smart contract developer is to take an existing business 6 process that might govern financial prices or delivery conditions, and express it as a smart contract in a programming language 7 8 Smart contracts primarily put, get and delete states in the world state, and can also query the state change history. 9 Chaincode “shim” APIs implements [ChaincodeStubInterface](https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim#ChaincodeStubInterface) 10 which contain methods for access and modify the ledger, and to make invocations between chaincodes. Main methods are: 11 12 * `GetState(key string) ([]byte, error)` performs a query to retrieve information about the current state of a business object 13 14 * `PutState(key string, value []byte) error` creates a new business object or modifies an existing one in the ledger world state 15 16 * `DelState(key string) error` removes of a business object from the current state of the ledger, but not its history 17 18 * `GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)` 19 queries the state in the ledger based on given partial composite key 20 21 * `GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error)` returns a history of key values across time. 22 23 24 All this methods use string key as record identifier and slice of bytes as state value. Most of examples uses JSON documents as 25 chaincode state value. Hyperledger Fabric supports both LevelDB as CouchDB to serve as state database, holding the latest state of each object. 26 LevelDB is the default key-value state database embedded in every peer. CouchDB is an optional alternative external 27 state database with more features - it supports rich queries against JSON documents in chaincode state, whereas LevelDB only supports 28 queries against keys. 29 30 31 ## Querying and updating state with ChaincodeStubInterface methods 32 33 As shown in many examples, assets can be represented as complex structures - Golang structs. 34 The chaincode itself can store data as a string in a key/value pair setup. Thus, we need to marshal struct to JSON string 35 before putting into chaincode state and unmarshal after getting from state. 36 37 With `ChaincodeStubInterface` methods these operations looks like 38 [this](https://github.com/IBM/build-blockchain-insurance-app/blob/master/web/chaincode/src/bcins/invoke_insurance.go): 39 40 ```go 41 ct := ContractType{} 42 43 err := json.Unmarshal([]byte(args[0]), &req) 44 if err != nil { 45 return shim.Error(err.Error()) 46 } 47 48 key, err := stub.CreateCompositeKey(prefixContractType, []string{req.UUID}) 49 if err != nil { 50 return shim.Error(err.Error()) 51 } 52 53 valAsBytes, err := stub.GetState(key) 54 if err != nil { 55 return shim.Error(err.Error()) 56 } 57 if len(valAsBytes) == 0 { 58 return shim.Error("Contract Type could not be found") 59 } 60 err = json.Unmarshal(valAsBytes, &ct) 61 if err != nil { 62 return shim.Error(err.Error()) 63 } 64 65 ct.Active = req.Active 66 67 valAsBytes, err = json.Marshal(ct) 68 if err != nil { 69 return shim.Error(err.Error()) 70 } 71 72 err = stub.PutState(key, valAsBytes) 73 if err != nil { 74 return shim.Error(err.Error()) 75 } 76 77 return shim.Success(nil) 78 ``` 79 80 In the example above smart contract code explicitly performs many auxiliary actions: 81 82 * Creating composite key 83 * Unmarshaling data after receiving it from state 84 * Marshaling data before placing it to state 85 86 ## Modelling chaincode state with CCKit 87 88 ### State methods wrapper 89 90 CCKit contains [wrapper](state.go) on `ChaincodeStubInterface` methods to working with chaincode state. This methods 91 simplifies chaincode key creation and data transformation during working with chaincode state. 92 93 ```go 94 type State interface { 95 // Get returns value from state, converted to target type 96 // entry can be Key (string or []string) or type implementing Keyer interface 97 Get(entry interface{}, target ...interface{}) (result interface{}, err error) 98 99 // Get returns value from state, converted to int 100 // entry can be Key (string or []string) or type implementing Keyer interface 101 GetInt(entry interface{}, defaultValue int) (result int, err error) 102 103 // GetHistory returns slice of history records for entry, with values converted to target type 104 // entry can be Key (string or []string) or type implementing Keyer interface 105 GetHistory(entry interface{}, target interface{}) (result HistoryEntryList, err error) 106 107 // Exists returns entry existence in state 108 // entry can be Key (string or []string) or type implementing Keyer interface 109 Exists(entry interface{}) (exists bool, err error) 110 111 // Put returns result of putting entry to state 112 // entry can be Key (string or []string) or type implementing Keyer interface 113 // if entry is implements Keyer interface and it's struct or type implementing 114 // ToByter interface value can be omitted 115 Put(entry interface{}, value ...interface{}) (err error) 116 117 // Insert returns result of inserting entry to state 118 // If same key exists in state error wil be returned 119 // entry can be Key (string or []string) or type implementing Keyer interface 120 // if entry is implements Keyer interface and it's struct or type implementing 121 // ToByter interface value can be omitted 122 Insert(entry interface{}, value ...interface{}) (err error) 123 124 // List returns slice of target type 125 // namespace can be part of key (string or []string) or entity with defined mapping 126 List(namespace interface{}, target ...interface{}) (result []interface{}, err error) 127 128 // Delete returns result of deleting entry from state 129 // entry can be Key (string or []string) or type implementing Keyer interface 130 Delete(entry interface{}) (err error) 131 132 ... 133 } 134 ``` 135 136 ### Converting from/to bytes while operating with chaincode state 137 138 State wrapper allows to automatically marshal golang type to/from slice of bytes. This type can be: 139 140 * Any type implementing [ToByter and FromByter](../convert/convert.go) interface 141 142 ```go 143 type ( 144 // FromByter interface supports FromBytes func for converting from slice of bytes to target type 145 FromByter interface { 146 FromBytes([]byte) (interface{}, error) 147 } 148 149 // ToByter interface supports ToBytes func for converting to slice of bytes from source type 150 ToByter interface { 151 ToBytes() ([]byte, error) 152 } 153 ) 154 ``` 155 156 * Golang struct or one of supported types ( `int`, `string`, `[]string`) 157 * [Protobuf](https://developers.google.com/protocol-buffers/docs/gotutorial) golang struct 158 159 160 Golang structs [automatically](../convert) marshals/ unmarshals using [json.Marshal](https://golang.org/pkg/encoding/json/#Marshal) and 161 and [json.Umarshal](https://golang.org/pkg/encoding/json/#Unmarshal) methods. 162 [proto.Marshal](https://godoc.org/github.com/golang/protobuf/proto#Marshal) and 163 [proto.Unmarshal](https://godoc.org/github.com/golang/protobuf/proto#Unmarshal) is used to convert protobuf. 164 165 ### Creating state keys 166 167 In the chaincode data model we often need to store many instances of one type on the ledger, such as multiple commercial papers, 168 letters of credit, and so on. In this case, the keys of those instances will be typically constructed from a combination of attributes— 169 for example: 170 171 > `CommercialPaper` + {Issuer} + {PaperId} 172 173 yielding series of chaincode state entries keys [ `CommercialPaperIssuer1Id1`, `CommercialPaperIssuer2Id2`, ...] 174 175 176 The logic of creation primary key of an instance can be customized in the code, or API functions can be provided in SHIM to 177 construct a composite key (in other words, a unique key) of an instance based on 178 a combination of several attributes. Composite keys can then be used as a normal string key to 179 record and retrieve values using the PutState() and GetState() functions. 180 181 The following snippet shows a list of functions that create and work with composite keys: 182 183 ```go 184 // The function creates a key by combining the attributes into a single string. 185 // The arguments must be valid utf8 strings and must not contain U+0000 (nil byte) and U+10FFFF charactres. 186 func CreateCompositeKey(objectType string, attributes []string) (string, error) 187 188 // The function splits the compositeKey into attributes from which the key was formed. 189 // This function is useful for extracting attributes from keys returned by range queries. 190 func SplitCompositeKey(compositeKey string) (string, []string, error) 191 ```` 192 193 When putting or getting data to/from chaincode state you must provide key. CCKit have 3 options for dealing with entries key: 194 195 * Key can be passed explicit: 196 197 ```go 198 c.State().Put ( `my-key`, &myStructInstance) 199 ``` 200 201 * Key type can implement [Keyer](state.go) interface 202 203 ```go 204 Key []string 205 206 // Keyer interface for entity containing logic of its key creation 207 Keyer interface { 208 Key() (Key, error) 209 } 210 ```` 211 212 `Key` type - is essentially slice of string, this slice will be automatically converted to string using `shim.CreateCompositeKey` method. 213 214 and in chaincode you need to provide only type instance 215 ```go 216 c.State().Put (&myStructInstance) 217 ``` 218 219 * Type can have associate mapping 220 221 Mapping defines rules for namespace, primary and other key creation. Mapping mainly used with `protobuf` state schema. 222 223 ### Range queries 224 225 As well as retrieving assets with a unique key, SHIM offers API functions the opportunity to retrieve sets of assets based on a range criteria. 226 Moreover, composite keys can be modeled to enable queries against multiple components of the key. 227 228 The range functions return an iterator (StateQueryIteratorInterface) over a set of keys matching the query criteria. The returned keys are in lexical order. 229 Additionally, when a composite key has multiple attributes, the range query function, `GetStateByPartialCompositeKey()`, can be used to search for keys matching a 230 subset of the attributes. 231 232 For example, the key of a `CommercialPaper` composed of `Issuer` and `PaperId` attributes can be searched for entries only from one Issuer. 233 234 ## Protobuf state example 235 236 This example uses [Commercial paper scenario](https://hyperledger-fabric.readthedocs.io/en/release-1.4/developapps/scenario.html) and 237 implements same functionality as [Node.JS](https://github.com/hyperledger/fabric-samples/tree/release-1.4/commercial-paper/organization/digibank/contract) 238 chaincode sample from official documentation. Example code located [here](../examples/cpaper). 239 240 241 Protobuf schema advantages: 242 243 1. Schema abstraction layer 244 245 Encoding the semantics of your business objects once, in proto format, is enough to help ensure that the signal doesn’t get lost between applications, 246 and that the boundaries you create enforce your business rules. 247 248 2. Extensions - validators etc 249 250 Protobuf v3 does not support validating required parameters, but there are third party projects for proto validation, for example 251 https://github.com/mwitkow/go-proto-validators. It allows to encode, at the schema level, the shape of your data structure, and the validation rules. 252 253 3. Easy Language Interoperability 254 Because Protocol Buffers are implemented in a variety of languages, they make interoperability between polyglot applications in your architecture that much simpler. 255 If you’re introducing a new service using Java or Node.Js SDK you simply have to hand the proto file to the code generator written in the target language and you 256 have guarantees about the safety and interoperability between those architectures. 257 258 259 260 ### Defining model 261 262 Protobuf (short for Protocol buffers) is a way of encoding structured data in an efficient and extensible format. 263 With protocol buffers, you write a .proto description of the data structure you wish to store. 264 From that, the protocol buffer compiler creates a golang struct (or ant) that implements automatic encoding and parsing 265 of the protocol buffer data with an efficient binary format. The generated class provides getters and setters 266 for the fields that make up a protocol buffer and takes care of the details of reading and writing the protocol 267 buffer as a unit. 268 269 In `Commercial Paper` example first we define messages, that will be stored in chaincode state or as events: 270 271 * `CommercialPaper` will be stored in chaincode state 272 * `CommercialPaperId` defines unique id part of commercial paper message 273 * `IssueCommercialPaper` payload for `issue` transaction and event triggered when new commercial paper issued 274 * `BuyCommercialPaper` payload for `buy` transaction and event triggered when commercial paper change owner 275 * `RedeemCommercialPaper` payload for `redeem` transaction and event triggered when commercial paper redeemed 276 277 ```proto 278 syntax = "proto3"; 279 package schema; 280 281 import "google/protobuf/timestamp.proto"; 282 283 message CommercialPaper { 284 285 enum State { 286 ISSUED = 0; 287 TRADING = 1; 288 REDEEMED = 2; 289 } 290 291 string issuer = 1; 292 string paper_number = 2; 293 string owner = 3; 294 google.protobuf.Timestamp issue_date = 4; 295 google.protobuf.Timestamp maturity_date = 5; 296 int32 face_value = 6; 297 State state = 7; 298 } 299 300 // CommercialPaperId identifier part 301 message CommercialPaperId { 302 string issuer = 1; 303 string paper_number = 2; 304 } 305 306 // IssueCommercialPaper event 307 message IssueCommercialPaper { 308 string issuer = 1; 309 string paper_number = 2; 310 google.protobuf.Timestamp issue_date = 3; 311 google.protobuf.Timestamp maturity_date = 4; 312 int32 face_value = 5; 313 } 314 315 // BuyCommercialPaper event 316 message BuyCommercialPaper { 317 string issuer = 1; 318 string paper_number = 2; 319 string current_owner = 3; 320 string new_owner = 4; 321 int32 price = 5; 322 google.protobuf.Timestamp purchase_date = 6; 323 } 324 325 // RedeemCommercialPaper event 326 message RedeemCommercialPaper { 327 string issuer = 1; 328 string paper_number = 2; 329 string redeeming_owner = 3; 330 google.protobuf.Timestamp redeem_date = 4; 331 } 332 ``` 333 334 ### Defining protobuf to chaincode state mapping 335 336 Protocol buffers to chaincode mapper can be used to store schema instances in chaincode state. Every schema type (protobuf or struct) can have mapping rules: 337 338 * Primary key creation logic 339 * Namespace logic 340 * Secondary key creation logic 341 342 For example, in definition below, we defined thant `schema.CommercialPaper` mapped to chaincode state with key attributes 343 from `schema.CommercialPaperId` message (`Issuer`, `PaperNumber`). Also we define event 344 345 ```go 346 var ( 347 // State mappings 348 StateMappings = m.StateMappings{}. 349 //key namespace will be <`CommercialPaper`, Issuer, PaperNumber> 350 Add(&schema.CommercialPaper{}, m.PKeySchema(&schema.CommercialPaperId{})) 351 352 // EventMappings 353 EventMappings = m.EventMappings{}. 354 // event name will be `IssueCommercialPaper`, payload - same as issue payload 355 Add(&schema.IssueCommercialPaper{}). 356 Add(&schema.BuyCommercialPaper{}). 357 Add(&schema.RedeemCommercialPaper{}) 358 ) 359 ``` 360 361 ### Chaincode 362 363 ```go 364 package cpaper 365 366 import ( 367 "fmt" 368 369 "github.com/pkg/errors" 370 "github.com/s7techlab/cckit/examples/cpaper/schema" 371 "github.com/s7techlab/cckit/extensions/debug" 372 "github.com/s7techlab/cckit/extensions/encryption" 373 "github.com/s7techlab/cckit/extensions/owner" 374 "github.com/s7techlab/cckit/router" 375 "github.com/s7techlab/cckit/router/param/defparam" 376 m "github.com/s7techlab/cckit/state/mapping" 377 ) 378 379 380 func NewCC() *router.Chaincode { 381 382 r := router.New(`commercial_paper`) 383 384 // Mappings for chaincode state 385 r.Use(m.MapStates(StateMappings)) 386 387 // Mappings for chaincode events 388 r.Use(m.MapEvents(EventMappings)) 389 390 // store in chaincode state information about chaincode first instantiator 391 r.Init(owner.InvokeSetFromCreator) 392 393 // method for debug chaincode state 394 debug.AddHandlers(r, `debug`, owner.Only) 395 396 r. 397 // read methods 398 Query(`list`, cpaperList). 399 400 // Get method has 2 params - commercial paper primary key components 401 Query(`get`, cpaperGet, defparam.Proto(&schema.CommercialPaperId{})). 402 403 // txn methods 404 Invoke(`issue`, cpaperIssue, defparam.Proto(&schema.IssueCommercialPaper{})). 405 Invoke(`buy`, cpaperBuy, defparam.Proto(&schema.BuyCommercialPaper{})). 406 Invoke(`redeem`, cpaperRedeem, defparam.Proto(&schema.RedeemCommercialPaper{})). 407 Invoke(`delete`, cpaperDelete, defparam.Proto(&schema.CommercialPaperId{})) 408 409 return router.NewChaincode(r) 410 } 411 412 413 func cpaperList(c router.Context) (interface{}, error) { 414 // commercial paper key is composite key <`CommercialPaper`>, {Issuer}, {PaperNumber} > 415 // where `CommercialPaper` - namespace of this type 416 // list method retrieves entries from chaincode state 417 // using GetStateByPartialCompositeKey method, then unmarshal received from state bytes via proto.Ummarshal method 418 // and creates slice of *schema.CommercialPaper 419 return c.State().List(&schema.CommercialPaper{}) 420 } 421 422 func cpaperIssue(c router.Context) (interface{}, error) { 423 var ( 424 issue = c.Param().(*schema.IssueCommercialPaper) //default parameter 425 cpaper = &schema.CommercialPaper{ 426 Issuer: issue.Issuer, 427 PaperNumber: issue.PaperNumber, 428 Owner: issue.Issuer, 429 IssueDate: issue.IssueDate, 430 MaturityDate: issue.MaturityDate, 431 FaceValue: issue.FaceValue, 432 State: schema.CommercialPaper_ISSUED, // initial state 433 } 434 err error 435 ) 436 437 if err = c.Event().Set(issue); err != nil { 438 return nil, err 439 } 440 441 return cpaper, c.State().Insert(cpaper) 442 } 443 444 func cpaperBuy(c router.Context) (interface{}, error) { 445 446 var ( 447 cpaper *schema.CommercialPaper 448 449 // but tx payload 450 buy = c.Param().(*schema.BuyCommercialPaper) 451 452 // current commercial paper state 453 cp, err = c.State().Get(&schema.CommercialPaper{ 454 Issuer: buy.Issuer, 455 PaperNumber: buy.PaperNumber}, &schema.CommercialPaper{}) 456 ) 457 458 if err != nil { 459 return nil, errors.Wrap(err, `not found`) 460 } 461 cpaper = cp.(*schema.CommercialPaper) 462 463 // Validate current owner 464 if cpaper.Owner != buy.CurrentOwner { 465 return nil, fmt.Errorf(`paper %s %s is not owned by %s`, cpaper.Issuer, cpaper.PaperNumber, buy.CurrentOwner) 466 } 467 468 // First buy moves state from ISSUED to TRADING 469 if cpaper.State == schema.CommercialPaper_ISSUED { 470 cpaper.State = schema.CommercialPaper_TRADING 471 } 472 473 // Check paper is not already REDEEMED 474 if cpaper.State == schema.CommercialPaper_TRADING { 475 cpaper.Owner = buy.NewOwner 476 } else { 477 return nil, fmt.Errorf(`paper %s %s is not trading.current state = %s`, cpaper.Issuer, cpaper.PaperNumber, cpaper.State) 478 } 479 480 if err = c.Event().Set(buy); err != nil { 481 return nil, err 482 } 483 return cpaper, c.State().Put(cpaper) 484 } 485 486 func cpaperRedeem(c router.Context) (interface{}, error) { 487 488 // implement me 489 490 return nil, nil 491 } 492 493 func cpaperGet(c router.Context) (interface{}, error) { 494 return c.State().Get(c.Param().(*schema.CommercialPaperId)) 495 } 496 497 func cpaperDelete(c router.Context) (interface{}, error) { 498 return nil, c.State().Delete(c.Param().(*schema.CommercialPaperId)) 499 } 500 ``` 501 502 ### Tests 503 We can [test](mapping/mapping_test.go) all chaincode use case scenarios using [MockStub](../testing) 504 505 ```go 506 package mapping_test 507 508 import ( 509 "testing" 510 511 "github.com/hyperledger/fabric/protos/peer" 512 513 "github.com/golang/protobuf/ptypes" 514 515 "github.com/golang/protobuf/proto" 516 "github.com/s7techlab/cckit/examples/cpaper/schema" 517 "github.com/s7techlab/cckit/examples/cpaper/testdata" 518 "github.com/s7techlab/cckit/state" 519 520 "github.com/s7techlab/cckit/examples/cpaper" 521 522 examplecert "github.com/s7techlab/cckit/examples/cert" 523 "github.com/s7techlab/cckit/identity" 524 testcc "github.com/s7techlab/cckit/testing" 525 expectcc "github.com/s7techlab/cckit/testing/expect" 526 527 . "github.com/onsi/ginkgo" 528 . "github.com/onsi/gomega" 529 ) 530 531 func TestState(t *testing.T) { 532 RegisterFailHandler(Fail) 533 RunSpecs(t, "State suite") 534 } 535 536 var ( 537 actors identity.Actors 538 cPaperCC *testcc.MockStub 539 err error 540 ) 541 var _ = Describe(`Mapping`, func() { 542 543 BeforeSuite(func() { 544 actors, err = identity.ActorsFromPemFile(`SOME_MSP`, map[string]string{ 545 `owner`: `s7techlab.pem`, 546 }, examplecert.Content) 547 548 Expect(err).To(BeNil()) 549 550 //Create commercial papers chaincode mock - protobuf based schema 551 cPaperCC = testcc.NewMockStub(`cpapers`, cpaper.NewCC()) 552 cPaperCC.From(actors[`owner`]).Init() 553 554 }) 555 556 Describe(`Protobuf based schema`, func() { 557 It("Allow to add data to chaincode state", func(done Done) { 558 559 events := cPaperCC.EventSubscription() 560 expectcc.ResponseOk(cPaperCC.Invoke(`issue`, &testdata.CPapers[0])) 561 562 Expect(<-events).To(BeEquivalentTo(&peer.ChaincodeEvent{ 563 EventName: `IssueCommercialPaper`, 564 Payload: testcc.MustProtoMarshal(&testdata.CPapers[0]), 565 })) 566 567 expectcc.ResponseOk(cPaperCC.Invoke(`issue`, &testdata.CPapers[1])) 568 expectcc.ResponseOk(cPaperCC.Invoke(`issue`, &testdata.CPapers[2])) 569 570 close(done) 571 }, 0.2) 572 573 It("Disallow to insert entries with same keys", func() { 574 expectcc.ResponseError(cPaperCC.Invoke(`issue`, &testdata.CPapers[0])) 575 }) 576 577 It("Allow to get entry list", func() { 578 cpapers := expectcc.PayloadIs(cPaperCC.Query(`list`), &[]schema.CommercialPaper{}).([]schema.CommercialPaper) 579 Expect(len(cpapers)).To(Equal(3)) 580 Expect(cpapers[0].Issuer).To(Equal(testdata.CPapers[0].Issuer)) 581 Expect(cpapers[0].PaperNumber).To(Equal(testdata.CPapers[0].PaperNumber)) 582 }) 583 584 It("Allow to get entry raw protobuf", func() { 585 cp := testdata.CPapers[0] 586 cpaperProtoFromCC := cPaperCC.Query(`get`, &schema.CommercialPaperId{Issuer: cp.Issuer, PaperNumber: cp.PaperNumber}).Payload 587 588 stateCpaper := &schema.CommercialPaper{ 589 Issuer: cp.Issuer, 590 PaperNumber: cp.PaperNumber, 591 Owner: cp.Issuer, 592 IssueDate: cp.IssueDate, 593 MaturityDate: cp.MaturityDate, 594 FaceValue: cp.FaceValue, 595 State: schema.CommercialPaper_ISSUED, // initial state 596 } 597 cPaperProto, _ := proto.Marshal(stateCpaper) 598 Expect(cpaperProtoFromCC).To(Equal(cPaperProto)) 599 }) 600 601 It("Allow update data in chaincode state", func() { 602 cp := testdata.CPapers[0] 603 expectcc.ResponseOk(cPaperCC.Invoke(`buy`, &schema.BuyCommercialPaper{ 604 Issuer: cp.Issuer, 605 PaperNumber: cp.PaperNumber, 606 CurrentOwner: cp.Issuer, 607 NewOwner: `some-new-owner`, 608 Price: cp.FaceValue - 10, 609 PurchaseDate: ptypes.TimestampNow(), 610 })) 611 612 cpaperFromCC := expectcc.PayloadIs( 613 cPaperCC.Query(`get`, &schema.CommercialPaperId{Issuer: cp.Issuer, PaperNumber: cp.PaperNumber}), 614 &schema.CommercialPaper{}).(*schema.CommercialPaper) 615 616 // state is updated 617 Expect(cpaperFromCC.State).To(Equal(schema.CommercialPaper_TRADING)) 618 Expect(cpaperFromCC.Owner).To(Equal(`some-new-owner`)) 619 }) 620 621 It("Allow to delete entry", func() { 622 623 cp := testdata.CPapers[0] 624 toDelete := &schema.CommercialPaperId{Issuer: cp.Issuer, PaperNumber: cp.PaperNumber} 625 626 expectcc.ResponseOk(cPaperCC.Invoke(`delete`, toDelete)) 627 cpapers := expectcc.PayloadIs(cPaperCC.Invoke(`list`), &[]schema.CommercialPaper{}).([]schema.CommercialPaper) 628 629 Expect(len(cpapers)).To(Equal(2)) 630 expectcc.ResponseError(cPaperCC.Invoke(`get`, toDelete), state.ErrKeyNotFound) 631 }) 632 }) 633 634 }) 635 ```