Add ER diagram and schema docs, enhance tx validation
Added ER.svg and database_schema.md to document the logical data model. Refactored and extended transaction validation in blockchain/abci.go to enforce ID prefix rules, entity existence, and uniqueness for promises, commitments, commiters, and beneficiaries. Updated lbc_sdk dependency version in go.mod and go.sum.
This commit is contained in:
parent
57c40d9f07
commit
dae1cd484e
5 changed files with 236 additions and 79 deletions
|
|
@ -33,6 +33,19 @@ func NewPromiseApp(db *badger.DB) *PromiseApp {
|
||||||
return &PromiseApp{db: db}
|
return &PromiseApp{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasPrefix(id, pref string) bool { return strings.HasPrefix(id, pref+":") }
|
||||||
|
|
||||||
|
func requireIDPrefix(id, pref string) error {
|
||||||
|
if strings.TrimSpace(id) == "" {
|
||||||
|
return fmt.Errorf("missing %s id", pref)
|
||||||
|
}
|
||||||
|
if !hasPrefix(id, pref) {
|
||||||
|
return fmt.Errorf("invalid %s id prefix", pref)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, error) {
|
func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, error) {
|
||||||
var outer struct {
|
var outer struct {
|
||||||
Body types.CommiterTxBody `json:"body"`
|
Body types.CommiterTxBody `json:"body"`
|
||||||
|
|
@ -83,73 +96,162 @@ func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, err
|
||||||
func (app *PromiseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
|
func (app *PromiseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
|
||||||
compound, err := verifyCompoundTx(app.db, req.Tx)
|
compound, err := verifyCompoundTx(app.db, req.Tx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Проверка на уникальность Promise.ID
|
// ---- Валидация содержимого композита по ER ----
|
||||||
if compound.Body.Promise != nil {
|
p := compound.Body.Promise
|
||||||
err := app.db.View(func(txn *badger.Txn) error {
|
c := compound.Body.Commitment
|
||||||
_, err := txn.Get([]byte(compound.Body.Promise.ID))
|
|
||||||
if err == badger.ErrKeyNotFound {
|
// Оба тела должны присутствовать
|
||||||
return nil
|
if p == nil || c == nil {
|
||||||
}
|
return abci.ResponseCheckTx{Code: 1, Log: "compound must include promise and commitment"}
|
||||||
return errors.New("duplicate promise ID")
|
}
|
||||||
})
|
|
||||||
if err != nil {
|
// ID-предикаты и префиксы
|
||||||
|
if err := requireIDPrefix(p.ID, "promise"); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
||||||
|
}
|
||||||
|
if err := requireIDPrefix(c.ID, "commitment"); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
||||||
|
}
|
||||||
|
if err := requireIDPrefix(c.CommiterID, "commiter"); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
||||||
|
}
|
||||||
|
if err := requireIDPrefix(p.BeneficiaryID, "beneficiary"); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
||||||
|
}
|
||||||
|
if p.ParentPromiseID != nil {
|
||||||
|
if err := requireIDPrefix(*p.ParentPromiseID, "promise"); err != nil {
|
||||||
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
||||||
}
|
}
|
||||||
}
|
if *p.ParentPromiseID == p.ID {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: "parent_promise_id must not equal promise id"}
|
||||||
// Проверка на уникальность Commitment.ID
|
|
||||||
if compound.Body.Commitment != nil {
|
|
||||||
err := app.db.View(func(txn *badger.Txn) error {
|
|
||||||
_, err := txn.Get([]byte(compound.Body.Commitment.ID))
|
|
||||||
if err == badger.ErrKeyNotFound {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New("duplicate commitment ID")
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка на существование коммитера
|
// Базовые обязательные поля Promise
|
||||||
if compound.Body.Commitment != nil {
|
if strings.TrimSpace(p.Text) == "" {
|
||||||
err := app.db.View(func(txn *badger.Txn) error {
|
return abci.ResponseCheckTx{Code: 2, Log: "promise.text is required"}
|
||||||
_, err := txn.Get([]byte(compound.Body.Commitment.CommiterID))
|
}
|
||||||
if err == badger.ErrKeyNotFound {
|
if p.Due == 0 {
|
||||||
return errors.New("unknown commiter")
|
return abci.ResponseCheckTx{Code: 2, Log: "promise.due is required"}
|
||||||
|
}
|
||||||
|
// Commitment due
|
||||||
|
if c.Due == 0 {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: "commitment.due is required"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Связность по ER
|
||||||
|
if c.PromiseID != p.ID {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: "commitment.promise_id must equal promise.id"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Уникальность Promise.ID
|
||||||
|
if err := app.db.View(func(txn *badger.Txn) error {
|
||||||
|
_, e := txn.Get([]byte(p.ID))
|
||||||
|
if e == badger.ErrKeyNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("duplicate promise ID")
|
||||||
|
}); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
|
||||||
|
}
|
||||||
|
// Уникальность Commitment.ID
|
||||||
|
if err := app.db.View(func(txn *badger.Txn) error {
|
||||||
|
_, e := txn.Get([]byte(c.ID))
|
||||||
|
if e == badger.ErrKeyNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("duplicate commitment ID")
|
||||||
|
}); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Существование коммитера (у тебя уже было — оставляю)
|
||||||
|
if err := app.db.View(func(txn *badger.Txn) error {
|
||||||
|
_, e := txn.Get([]byte(c.CommiterID))
|
||||||
|
if e == badger.ErrKeyNotFound {
|
||||||
|
return errors.New("unknown commiter")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 4, Log: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Существование бенефициара
|
||||||
|
if err := app.db.View(func(txn *badger.Txn) error {
|
||||||
|
_, e := txn.Get([]byte(p.BeneficiaryID))
|
||||||
|
if e == badger.ErrKeyNotFound {
|
||||||
|
return errors.New("unknown beneficiary")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 5, Log: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Существование parent (если задан)
|
||||||
|
if p.ParentPromiseID != nil {
|
||||||
|
if err := app.db.View(func(txn *badger.Txn) error {
|
||||||
|
_, e := txn.Get([]byte(*p.ParentPromiseID))
|
||||||
|
if e == badger.ErrKeyNotFound {
|
||||||
|
return errors.New("unknown parent promise")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
return abci.ResponseCheckTx{Code: 6, Log: err.Error()}
|
||||||
return abci.ResponseCheckTx{Code: 4, Log: err.Error()}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return abci.ResponseCheckTx{Code: 0}
|
return abci.ResponseCheckTx{Code: 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Попытка старого формата только если он реально валиден;
|
// ---- Попытка ОДИНОЧНЫХ транзакций ----
|
||||||
// иначе возвращаем исходную причину из verifyCompoundTx.
|
|
||||||
|
// 3.1) Совместимость: одиночный commiter (твоя старая логика)
|
||||||
if body, oldErr := verifyAndExtractBody(app.db, req.Tx); oldErr == nil {
|
if body, oldErr := verifyAndExtractBody(app.db, req.Tx); oldErr == nil {
|
||||||
id, ok := body["id"].(string)
|
id, ok := body["id"].(string)
|
||||||
if !ok || id == "" {
|
if !ok || id == "" {
|
||||||
return abci.ResponseCheckTx{Code: 2, Log: "missing id"}
|
return abci.ResponseCheckTx{Code: 2, Log: "missing id"}
|
||||||
}
|
}
|
||||||
// Проверка на дубликат
|
if err := requireIDPrefix(id, "commiter"); err != nil {
|
||||||
err := app.db.View(func(txn *badger.Txn) error {
|
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
||||||
_, err := txn.Get([]byte(id))
|
}
|
||||||
if err == badger.ErrKeyNotFound {
|
// Дубликат
|
||||||
|
if err := app.db.View(func(txn *badger.Txn) error {
|
||||||
|
_, e := txn.Get([]byte(id))
|
||||||
|
if e == badger.ErrKeyNotFound {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return errors.New("duplicate id")
|
return errors.New("duplicate id")
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
|
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
|
||||||
}
|
}
|
||||||
return abci.ResponseCheckTx{Code: 0}
|
return abci.ResponseCheckTx{Code: 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Здесь: составной формат не прошёл — вернём ПРИЧИНУ.
|
var single struct {
|
||||||
|
Body types.BeneficiaryTxBody `json:"body"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
if err2 := json.Unmarshal(req.Tx, &single); err2 == nil && single.Body.Type == "beneficiary" {
|
||||||
|
if err := requireIDPrefix(single.Body.ID, "beneficiary"); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(single.Body.Name) == "" {
|
||||||
|
return abci.ResponseCheckTx{Code: 2, Log: "beneficiary.name is required"}
|
||||||
|
}
|
||||||
|
// уникальность
|
||||||
|
if err := app.db.View(func(txn *badger.Txn) error {
|
||||||
|
_, e := txn.Get([]byte(single.Body.ID))
|
||||||
|
if e == badger.ErrKeyNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("duplicate beneficiary ID")
|
||||||
|
}); err != nil {
|
||||||
|
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
|
||||||
|
}
|
||||||
|
return abci.ResponseCheckTx{Code: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если дошли сюда — составной формат не прошёл; вернём его причину.
|
||||||
return abci.ResponseCheckTx{Code: 1, Log: err.Error()}
|
return abci.ResponseCheckTx{Code: 1, Log: err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,59 +264,68 @@ func (app *PromiseApp) BeginBlock(_ abci.RequestBeginBlock) abci.ResponseBeginBl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
|
func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
|
||||||
compound, err := verifyCompoundTx(app.db, req.Tx)
|
// Попытка композита
|
||||||
if err != nil {
|
if compound, err := verifyCompoundTx(app.db, req.Tx); err == nil {
|
||||||
outer := struct {
|
|
||||||
Body types.CommiterTxBody `json:"body"`
|
|
||||||
Signature string `json:"signature"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(req.Tx, &outer); err != nil {
|
|
||||||
return abci.ResponseDeliverTx{Code: 1, Log: "invalid tx format"}
|
|
||||||
}
|
|
||||||
// Валидация подписи/ключа одиночного коммитера перед записью
|
|
||||||
if _, vErr := verifyAndExtractBody(app.db, req.Tx); vErr != nil {
|
|
||||||
return abci.ResponseDeliverTx{Code: 1, Log: vErr.Error()}
|
|
||||||
}
|
|
||||||
|
|
||||||
id := outer.Body.ID
|
|
||||||
if id == "" {
|
|
||||||
return abci.ResponseDeliverTx{Code: 1, Log: "missing id"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.currentBatch == nil {
|
if app.currentBatch == nil {
|
||||||
app.currentBatch = app.db.NewTransaction(true)
|
app.currentBatch = app.db.NewTransaction(true)
|
||||||
}
|
}
|
||||||
|
if compound.Body.Promise != nil {
|
||||||
data, err := json.Marshal(outer.Body)
|
data, _ := json.Marshal(compound.Body.Promise)
|
||||||
if err != nil {
|
if err := app.currentBatch.Set([]byte(compound.Body.Promise.ID), data); err != nil {
|
||||||
return abci.ResponseDeliverTx{Code: 1, Log: err.Error()}
|
return abci.ResponseDeliverTx{Code: 1, Log: "failed to save promise"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err = app.currentBatch.Set([]byte(id), data); err != nil {
|
if compound.Body.Commitment != nil {
|
||||||
return abci.ResponseDeliverTx{Code: 1, Log: err.Error()}
|
data, _ := json.Marshal(compound.Body.Commitment)
|
||||||
|
if err := app.currentBatch.Set([]byte(compound.Body.Commitment.ID), data); err != nil {
|
||||||
|
return abci.ResponseDeliverTx{Code: 1, Log: "failed to save commitment"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return abci.ResponseDeliverTx{Code: 0}
|
return abci.ResponseDeliverTx{Code: 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.currentBatch == nil {
|
// Одиночный commiter (как раньше)
|
||||||
app.currentBatch = app.db.NewTransaction(true)
|
{
|
||||||
}
|
var outer struct {
|
||||||
|
Body types.CommiterTxBody `json:"body"`
|
||||||
if compound.Body.Promise != nil {
|
Signature string `json:"signature"`
|
||||||
data, _ := json.Marshal(compound.Body.Promise)
|
|
||||||
err := app.currentBatch.Set([]byte(compound.Body.Promise.ID), data)
|
|
||||||
if err != nil {
|
|
||||||
return abci.ResponseDeliverTx{Code: 1, Log: "failed to save promise"}
|
|
||||||
}
|
}
|
||||||
}
|
if err := json.Unmarshal(req.Tx, &outer); err == nil && outer.Body.Type == "commiter" {
|
||||||
if compound.Body.Commitment != nil {
|
// сигнатуру проверяем прежней функцией
|
||||||
data, _ := json.Marshal(compound.Body.Commitment)
|
if _, vErr := verifyAndExtractBody(app.db, req.Tx); vErr != nil {
|
||||||
err := app.currentBatch.Set([]byte(compound.Body.Commitment.ID), data)
|
return abci.ResponseDeliverTx{Code: 1, Log: vErr.Error()}
|
||||||
if err != nil {
|
}
|
||||||
return abci.ResponseDeliverTx{Code: 1, Log: "failed to save commitment"}
|
if app.currentBatch == nil {
|
||||||
|
app.currentBatch = app.db.NewTransaction(true)
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(outer.Body)
|
||||||
|
if err := app.currentBatch.Set([]byte(outer.Body.ID), data); err != nil {
|
||||||
|
return abci.ResponseDeliverTx{Code: 1, Log: err.Error()}
|
||||||
|
}
|
||||||
|
return abci.ResponseDeliverTx{Code: 0}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return abci.ResponseDeliverTx{Code: 0}
|
// Одиночный beneficiary
|
||||||
|
{
|
||||||
|
var outer struct {
|
||||||
|
Body types.BeneficiaryTxBody `json:"body"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.Tx, &outer); err == nil && outer.Body.Type == "beneficiary" {
|
||||||
|
// (пока без проверки подписи — можно добавить политику позже)
|
||||||
|
if app.currentBatch == nil {
|
||||||
|
app.currentBatch = app.db.NewTransaction(true)
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(outer.Body)
|
||||||
|
if err := app.currentBatch.Set([]byte(outer.Body.ID), data); err != nil {
|
||||||
|
return abci.ResponseDeliverTx{Code: 1, Log: err.Error()}
|
||||||
|
}
|
||||||
|
return abci.ResponseDeliverTx{Code: 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return abci.ResponseDeliverTx{Code: 1, Log: "invalid tx format"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyCompoundTx(db *badger.DB, tx []byte) (*types.CompoundTx, error) {
|
func verifyCompoundTx(db *badger.DB, tx []byte) (*types.CompoundTx, error) {
|
||||||
|
|
|
||||||
1
docs/ER.svg
Normal file
1
docs/ER.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
43
docs/database_schema.md
Normal file
43
docs/database_schema.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Логическая модель данных
|
||||||
|
|
||||||
|
![[ER.svg]]
|
||||||
|
|
||||||
|
<details>
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
entity Promise {
|
||||||
|
* ID: uuid
|
||||||
|
--
|
||||||
|
* text: text
|
||||||
|
* due: datetime
|
||||||
|
BeneficiaryID: uuid
|
||||||
|
ParentPromiseID: uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
entity Beneficiary {
|
||||||
|
* ID: uuid
|
||||||
|
--
|
||||||
|
* name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
entity Commitment {
|
||||||
|
* ID: uuid
|
||||||
|
--
|
||||||
|
PromiseID: int
|
||||||
|
CommiterID: int
|
||||||
|
due: datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
entity Commiter {
|
||||||
|
* ID: int
|
||||||
|
--
|
||||||
|
* name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
Commitment }|--|| Promise : belongs to
|
||||||
|
Commitment }|--|| Commiter : made by
|
||||||
|
Promise }o--|| Beneficiary : has
|
||||||
|
Promise }--o Promise : parent of
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
</details>
|
||||||
2
go.mod
2
go.mod
|
|
@ -68,7 +68,7 @@ require (
|
||||||
github.com/google/orderedcode v0.0.1 // indirect
|
github.com/google/orderedcode v0.0.1 // indirect
|
||||||
github.com/google/pprof v0.0.0-20241017200806-017d972448fc // indirect
|
github.com/google/pprof v0.0.0-20241017200806-017d972448fc // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/gregorybednov/lbc_sdk v0.0.0-20250810102513-432a51e65f76 // indirect
|
github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa // indirect
|
||||||
github.com/gtank/merlin v0.1.1 // indirect
|
github.com/gtank/merlin v0.1.1 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/hjson/hjson-go/v4 v4.4.0 // indirect
|
github.com/hjson/hjson-go/v4 v4.4.0 // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -197,6 +197,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gregorybednov/lbc_sdk v0.0.0-20250810102513-432a51e65f76 h1:e3A+1v+Mjt8nuJcVnHuhHZuh4052KRLCUBpH4g74rVs=
|
github.com/gregorybednov/lbc_sdk v0.0.0-20250810102513-432a51e65f76 h1:e3A+1v+Mjt8nuJcVnHuhHZuh4052KRLCUBpH4g74rVs=
|
||||||
github.com/gregorybednov/lbc_sdk v0.0.0-20250810102513-432a51e65f76/go.mod h1:DBE00+SaYBtD4qw+nOtSTLuF6h9Ia4TkuBMJB+6krik=
|
github.com/gregorybednov/lbc_sdk v0.0.0-20250810102513-432a51e65f76/go.mod h1:DBE00+SaYBtD4qw+nOtSTLuF6h9Ia4TkuBMJB+6krik=
|
||||||
|
github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa h1:8EuqAmsS94ju83o4aEIV8e2fdocdAJ9xVerKRSI+nDA=
|
||||||
|
github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa/go.mod h1:DBE00+SaYBtD4qw+nOtSTLuF6h9Ia4TkuBMJB+6krik=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
|
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
|
||||||
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue