From 298a6a9750776193f59918443fbdfb01cbddde29 Mon Sep 17 00:00:00 2001 From: Arne Date: Mon, 29 Apr 2024 16:25:30 +0200 Subject: [PATCH] add ability to query for token details (#612) Signed-off-by: Arne Rutjes --- token/sdk/tokens/authrorization.go | 10 + token/services/auditdb/db.go | 2 +- token/services/db/dbtest/transactions.go | 107 ++++++--- token/services/db/driver/common.go | 5 + token/services/db/driver/token.go | 57 ++++- token/services/db/driver/ttx.go | 2 +- token/services/db/sql/querybuilder.go | 209 ++++++++-------- token/services/db/sql/querybuilder_test.go | 164 ++++++++++--- token/services/db/sql/tokens.go | 264 +++++++++++++-------- token/services/db/sql/tokens_test.go | 113 ++++++--- token/services/db/sql/transactions.go | 47 ++-- token/services/interop/htlc/script.go | 8 + token/services/tokens/manager.go | 2 +- token/services/tokens/storage.go | 32 ++- token/services/tokens/tokens.go | 4 +- token/services/ttxdb/db.go | 5 +- 16 files changed, 686 insertions(+), 345 deletions(-) diff --git a/token/sdk/tokens/authrorization.go b/token/sdk/tokens/authrorization.go index 76002a3b4..4da7a1629 100644 --- a/token/sdk/tokens/authrorization.go +++ b/token/sdk/tokens/authrorization.go @@ -8,6 +8,7 @@ package tokens import ( "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) @@ -75,3 +76,12 @@ func (o *AuthorizationMultiplexer) AmIAnAuditor(tms *token.ManagementService) bo } return false } + +// OwnerType returns the type of owner (e.g. 'idemix' or 'htlc') and the identity bytes +func (o *AuthorizationMultiplexer) OwnerType(raw []byte) (string, []byte, error) { + owner, err := identity.UnmarshalTypedIdentity(raw) + if err != nil { + return "", nil, err + } + return owner.Type, owner.Identity, nil +} diff --git a/token/services/auditdb/db.go b/token/services/auditdb/db.go index 39d1126b9..a9e88aef0 100644 --- a/token/services/auditdb/db.go +++ b/token/services/auditdb/db.go @@ -182,7 +182,7 @@ func (d *DB) Append(req *token.Request) error { if err != nil { return errors.WithMessagef(err, "begin update for txid [%s] failed", record.Anchor) } - if err := w.AddTokenRequest(record.Anchor, raw); err != nil { + if err := w.AddTokenRequest(record.Anchor, raw, req.Metadata.Application); err != nil { w.Rollback() return errors.WithMessagef(err, "append token request for txid [%s] failed", record.Anchor) } diff --git a/token/services/db/dbtest/transactions.go b/token/services/db/dbtest/transactions.go index 2a6655b7d..eefbecfda 100644 --- a/token/services/db/dbtest/transactions.go +++ b/token/services/db/dbtest/transactions.go @@ -95,7 +95,7 @@ func TStatus(t *testing.T, db driver.TokenTransactionDB) { w, err := db.BeginAtomicWrite() assert.NoError(t, err, "begin") - assert.NoError(t, w.AddTokenRequest("tx1", []byte("request")), "add token request") + assert.NoError(t, w.AddTokenRequest("tx1", []byte("request"), map[string][]byte{}), "add token request") assert.NoError(t, w.AddTransaction(&tx)) assert.NoError(t, w.AddValidationRecord("tx1", nil), "add validation record") assert.NoError(t, w.AddMovement(&mv)) @@ -134,7 +134,7 @@ func TStatus(t *testing.T, db driver.TokenTransactionDB) { func TStoresTimestamp(t *testing.T, db driver.TokenTransactionDB) { w, err := db.BeginAtomicWrite() assert.NoError(t, err) - assert.NoError(t, w.AddTokenRequest("tx1", []byte(""))) + assert.NoError(t, w.AddTokenRequest("tx1", []byte(""), map[string][]byte{})) assert.NoError(t, w.AddTransaction(&driver.TransactionRecord{ TxID: "tx1", ActionType: driver.Transfer, @@ -164,9 +164,9 @@ func TStoresTimestamp(t *testing.T, db driver.TokenTransactionDB) { func TMovements(t *testing.T, db driver.TokenTransactionDB) { w, err := db.BeginAtomicWrite() assert.NoError(t, err) - assert.NoError(t, w.AddTokenRequest("0", []byte{})) - assert.NoError(t, w.AddTokenRequest("1", []byte{})) - assert.NoError(t, w.AddTokenRequest("2", []byte{})) + assert.NoError(t, w.AddTokenRequest("0", []byte{}, map[string][]byte{})) + assert.NoError(t, w.AddTokenRequest("1", []byte{}, map[string][]byte{})) + assert.NoError(t, w.AddTokenRequest("2", []byte{}, map[string][]byte{})) assert.NoError(t, w.AddMovement(&driver.MovementRecord{ TxID: "0", EnrollmentID: "alice", @@ -235,15 +235,16 @@ func TTransaction(t *testing.T, db driver.TokenTransactionDB) { w, err := db.BeginAtomicWrite() assert.NoError(t, err) tr1 := &driver.TransactionRecord{ - TxID: fmt.Sprintf("tx%d", 99), - ActionType: driver.Transfer, - SenderEID: "bob", - RecipientEID: "alice", - TokenType: "magic", - Amount: big.NewInt(10), - Timestamp: lastYear, - } - assert.NoError(t, w.AddTokenRequest(tr1.TxID, []byte(fmt.Sprintf("token request for %s", tr1.TxID)))) + TxID: fmt.Sprintf("tx%d", 99), + ActionType: driver.Transfer, + SenderEID: "bob", + RecipientEID: "alice", + TokenType: "magic", + Amount: big.NewInt(10), + ApplicationMetadata: map[string][]byte{}, + Timestamp: lastYear, + } + assert.NoError(t, w.AddTokenRequest(tr1.TxID, []byte(fmt.Sprintf("token request for %s", tr1.TxID)), map[string][]byte{})) assert.NoError(t, w.AddTransaction(tr1)) for i := 0; i < 20; i++ { @@ -256,13 +257,22 @@ func TTransaction(t *testing.T, db driver.TokenTransactionDB) { TokenType: "magic", Amount: big.NewInt(10), Timestamp: now, + ApplicationMetadata: map[string][]byte{ + "this is the first key": {99, 33, 22, 11}, + "this is the second key": []byte("with some text as the value " + fmt.Sprintf("tx%d", i)), + }, } - assert.NoError(t, w.AddTokenRequest(tr1.TxID, []byte(fmt.Sprintf("token request for %s", tr1.TxID)))) + assert.NoError(t, w.AddTokenRequest(tr1.TxID, []byte(fmt.Sprintf("token request for %s", tr1.TxID)), tr1.ApplicationMetadata)) assert.NoError(t, w.AddTransaction(tr1)) txs = append(txs, tr1) } assert.NoError(t, w.Commit()) + // get one + one := getTransactions(t, db, driver.QueryTransactionsParams{IDs: []string{"tx10"}}) + assert.Len(t, one, 1) + assert.Equal(t, "tx10", one[0].TxID) + // get all except last year's t1 := time.Now().Add(time.Second * 3) it, err := db.QueryTransactions(driver.QueryTransactionsParams{From: &t0, To: &t1}) @@ -308,6 +318,28 @@ func TTransaction(t *testing.T, db driver.TokenTransactionDB) { status, _, err = db.GetStatus("nonexistenttx") assert.NoError(t, err, "a non existent transaction should return Unknown status but no error") assert.Equal(t, driver.Unknown, status) + + // exclude to self + w, err = db.BeginAtomicWrite() + assert.NoError(t, err) + tr1 = &driver.TransactionRecord{ + TxID: "1234", + ActionType: driver.Transfer, + SenderEID: "alice", + RecipientEID: "alice", + TokenType: "magic", + Amount: big.NewInt(10), + ApplicationMetadata: map[string][]byte{}, + Timestamp: lastYear, + } + assert.NoError(t, w.AddTokenRequest(tr1.TxID, []byte(fmt.Sprintf("token request for %s", tr1.TxID)), map[string][]byte{})) + assert.NoError(t, w.AddTransaction(tr1)) + assert.NoError(t, w.Commit()) + noChange := getTransactions(t, db, driver.QueryTransactionsParams{ExcludeToSelf: true}) + assert.Len(t, noChange, 21) + for _, tr := range noChange { + assert.NotEqual(t, tr.TxID, tr1.TxID, "transaction to self should not be included") + } } const explanation = "transactions [%s]=[%s]" @@ -329,6 +361,7 @@ func assertTxEqual(t *testing.T, exp *driver.TransactionRecord, act *driver.Tran assert.Equal(t, exp.RecipientEID, act.RecipientEID, expl) assert.Equal(t, exp.TokenType, act.TokenType, expl) assert.Equal(t, exp.Amount, act.Amount, expl) + assert.Equal(t, exp.ApplicationMetadata, act.ApplicationMetadata, expl) assert.WithinDuration(t, exp.Timestamp, act.Timestamp, 3*time.Second) } @@ -336,10 +369,10 @@ func TTokenRequest(t *testing.T, db driver.TokenTransactionDB) { w, err := db.BeginAtomicWrite() assert.NoError(t, err) tr1 := []byte("arbitrary bytes") - err = w.AddTokenRequest("id1", tr1) + err = w.AddTokenRequest("id1", tr1, map[string][]byte{}) assert.NoError(t, err) tr2 := []byte("arbitrary bytes 2") - err = w.AddTokenRequest("id2", tr2) + err = w.AddTokenRequest("id2", tr2, map[string][]byte{}) assert.NoError(t, err) assert.NoError(t, w.Commit()) assert.NoError(t, db.SetStatus("id2", driver.Confirmed, "")) @@ -439,27 +472,29 @@ func TTokenRequest(t *testing.T, db driver.TokenTransactionDB) { func TAllowsSameTxID(t *testing.T, db driver.TokenTransactionDB) { // bob sends 10 to alice tr1 := &driver.TransactionRecord{ - TxID: "1", - ActionType: driver.Transfer, - SenderEID: "bob", - RecipientEID: "alice", - TokenType: "magic", - Amount: big.NewInt(10), - Timestamp: time.Now(), + TxID: "1", + ActionType: driver.Transfer, + SenderEID: "bob", + RecipientEID: "alice", + TokenType: "magic", + ApplicationMetadata: map[string][]byte{}, + Amount: big.NewInt(10), + Timestamp: time.Now(), } // 1 is sent back to bobs wallet as change tr2 := &driver.TransactionRecord{ - TxID: "1", - ActionType: driver.Transfer, - SenderEID: "bob", - RecipientEID: "bob", - TokenType: "magic", - Amount: big.NewInt(1), - Timestamp: time.Now(), + TxID: "1", + ActionType: driver.Transfer, + SenderEID: "bob", + RecipientEID: "bob", + TokenType: "magic", + ApplicationMetadata: map[string][]byte{}, + Amount: big.NewInt(1), + Timestamp: time.Now(), } w, err := db.BeginAtomicWrite() assert.NoError(t, err) - assert.NoError(t, w.AddTokenRequest(tr1.TxID, []byte{})) + assert.NoError(t, w.AddTokenRequest(tr1.TxID, []byte{}, map[string][]byte{})) assert.NoError(t, w.AddTransaction(tr1)) assert.NoError(t, w.AddTransaction(tr2)) assert.NoError(t, w.Commit()) @@ -473,7 +508,7 @@ func TAllowsSameTxID(t *testing.T, db driver.TokenTransactionDB) { func TRollback(t *testing.T, db driver.TokenTransactionDB) { w, err := db.BeginAtomicWrite() assert.NoError(t, err) - assert.NoError(t, w.AddTokenRequest("1", []byte("arbitrary bytes"))) + assert.NoError(t, w.AddTokenRequest("1", []byte("arbitrary bytes"), map[string][]byte{})) mr1 := &driver.MovementRecord{ TxID: "1", @@ -718,7 +753,7 @@ func TTransactionQueries(t *testing.T, db driver.TokenTransactionDB) { var previous string for _, r := range tr { if r.TxID != previous { - assert.NoError(t, w.AddTokenRequest(r.TxID, []byte{})) + assert.NoError(t, w.AddTokenRequest(r.TxID, []byte{}, map[string][]byte{})) } assert.NoError(t, w.AddTransaction(&r)) previous = r.TxID @@ -790,7 +825,7 @@ func TValidationRecordQueries(t *testing.T, db driver.TokenTransactionDB) { w, err := db.BeginAtomicWrite() assert.NoError(t, err) for _, e := range exp { - assert.NoError(t, w.AddTokenRequest(e.TxID, e.TokenRequest)) + assert.NoError(t, w.AddTokenRequest(e.TxID, e.TokenRequest, map[string][]byte{})) assert.NoError(t, w.AddValidationRecord(e.TxID, e.Metadata), "AddValidationRecord "+e.TxID) } assert.NoError(t, w.Commit(), "Commit") @@ -877,7 +912,7 @@ func createTransaction(t *testing.T, db driver.TokenTransactionDB, txID string) if err != nil { t.Fatalf("error creating transaction while trying to test something else: %s", err) } - if err := w.AddTokenRequest(txID, []byte{}); err != nil { + if err := w.AddTokenRequest(txID, []byte{}, map[string][]byte{}); err != nil { t.Fatalf("error creating token request while trying to test something else: %s", err) } tr1 := &driver.TransactionRecord{ diff --git a/token/services/db/driver/common.go b/token/services/db/driver/common.go index 9b3df0feb..a7f798084 100644 --- a/token/services/db/driver/common.go +++ b/token/services/db/driver/common.go @@ -205,6 +205,11 @@ type QueryMovementsParams struct { // QueryTransactionsParams defines the parameters for querying transactions. // One can filter by sender, by recipient, and by time range. type QueryTransactionsParams struct { + // IDs is the list of transaction ids. If nil or empty, all transactions are returned + IDs []string + // ExcludeToSelf can be used to filter out 'change' transactions where the sender and + // recipient have the same enrollment id. + ExcludeToSelf bool // SenderWallet is the wallet of the sender // If empty, any sender is accepted // If the sender does not match but the recipient matches, the transaction is returned diff --git a/token/services/db/driver/token.go b/token/services/db/driver/token.go index dc6e7a330..7c9798cf4 100644 --- a/token/services/db/driver/token.go +++ b/token/services/db/driver/token.go @@ -7,6 +7,9 @@ SPDX-License-Identifier: Apache-2.0 package driver import ( + "errors" + "time" + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" token2 "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" @@ -21,8 +24,12 @@ type TokenRecord struct { // IssuerRaw represents the serialization of the issuer identity // if this is an IssuedToken. IssuerRaw []byte - // OwnerRaw is the serialization of the owner identity + // OwnerRaw is the serialization of the owner TypedIdentity OwnerRaw []byte + // OwnerType is the deserialized type inside OwnerRaw + OwnerType string + // OwnerIdentity is the deserialized Identity inside OwnerRaw + OwnerIdentity []byte // Ledger is the raw token as stored on the ledger Ledger []byte // LedgerMetadata is the metadata associated to the content of Ledger @@ -42,6 +49,46 @@ type TokenRecord struct { Issuer bool } +// TokenDetails provides details about an owned (spent or unspent) token +type TokenDetails struct { + // TxID is the ID of the transaction that created the token + TxID string + // Index is the index in the transaction + Index uint64 + // OwnerIdentity is the serialization of the owner identity + OwnerIdentity []byte + // OwnerType is the deserialized type inside OwnerRaw + OwnerType string + // OwnerEnrollment is the enrollment id of the owner + OwnerEnrollment string + // Type is the type of token + Type string + // Amount is the Quantity converted to decimal + Amount uint64 + // IsSpent is true if the token has been spent + IsSpent bool + // SpentBy is the transactionID that spent this token, if available + SpentBy string + // StoredAt is the moment the token was stored by this wallet + StoredAt time.Time +} + +// QueryTokenDetailsParams defines the parameters for querying token details +type QueryTokenDetailsParams struct { + // OwnerEnrollmentID is the optional owner of the token + OwnerEnrollmentID string + // OwnerType is the type of owner, for instance 'idemix' or 'htlc' + OwnerType string + // TokenType (optional) is the type of token + TokenType string + //IDs is an optional list of specific token ids to return + IDs []*token.ID + // TransactionIDs selects tokens that are the output of the provided transaction ids. + TransactionIDs []string + // IncludeDeleted determines whether to include spent tokens. It defaults to false. + IncludeDeleted bool +} + // CertificationDB defines a database to manager token certifications type CertificationDB interface { // ExistsCertification returns true if a certification for the passed token exists, @@ -61,7 +108,7 @@ type TokenDBTransaction interface { // TransactionExists returns true if a token with that transaction id exists in the db TransactionExists(id string) (bool, error) // GetToken returns the owned tokens and their identifier keys for the passed ids. - GetToken(txID string, index uint64, includeDeleted bool) (*token.Token, error) + GetToken(txID string, index uint64, includeDeleted bool) (*token.Token, []string, error) // OwnersOf returns the list of owner of a given token OwnersOf(txID string, index uint64) ([]string, error) // Delete marks the passed token as deleted by a given identifier (idempotent) @@ -114,6 +161,8 @@ type TokenDB interface { PublicParams() ([]byte, error) // NewTokenDBTransaction returns a new Transaction to commit atomically multiple operations NewTokenDBTransaction() (TokenDBTransaction, error) + // QueryTokenDetails provides detailed information about tokens + QueryTokenDetails(params QueryTokenDetailsParams) ([]TokenDetails, error) } // TokenDBDriver is the interface for a token database driver @@ -121,3 +170,7 @@ type TokenDBDriver interface { // Open opens a token database Open(sp view2.ServiceProvider, tmsID token2.TMSID) (TokenDB, error) } + +var ( + ErrTokenDoesNotExist = errors.New("token does not exist") +) diff --git a/token/services/db/driver/ttx.go b/token/services/db/driver/ttx.go index 0fff16412..59af78cd4 100644 --- a/token/services/db/driver/ttx.go +++ b/token/services/db/driver/ttx.go @@ -31,7 +31,7 @@ type AtomicWrite interface { Rollback() // AddTokenRequest binds the passed transaction id to the passed token request - AddTokenRequest(txID string, tr []byte) error + AddTokenRequest(txID string, tr []byte, applicationMetadata map[string][]byte) error // AddMovement adds a movement record to the database transaction. // Each token transaction can be seen as a list of movements. diff --git a/token/services/db/sql/querybuilder.go b/token/services/db/sql/querybuilder.go index b4b687fee..16eba73e1 100644 --- a/token/services/db/sql/querybuilder.go +++ b/token/services/db/sql/querybuilder.go @@ -12,121 +12,108 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/services/db/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/token" - "github.com/pkg/errors" ) -func transactionsConditionsSql(params driver.QueryTransactionsParams) (string, []interface{}) { - args := make([]interface{}, 0) - and := make([]string, 0) +func transactionsConditionsSql(params driver.QueryTransactionsParams, table string) (where string, args []any) { + and := []string{} + + // By id(s) + if len(params.IDs) > 0 { + colTxID := "tx_id" + if len(table) > 0 { + colTxID = fmt.Sprintf("%s.%s", table, colTxID) + } + and = append(and, in(&args, colTxID, params.IDs)) + } + // Filter out 'change' transactions + if params.ExcludeToSelf { + and = append(and, "(sender_eid != recipient_eid)") + } // Timestamp from if params.From != nil && !params.From.IsZero() { - where := fmt.Sprintf("stored_at >= %s", fmt.Sprintf("$%d", len(args)+1)) args = append(args, params.From.UTC()) + where := fmt.Sprintf("stored_at >= %s", fmt.Sprintf("$%d", len(args))) and = append(and, where) } // Timestamp to if params.To != nil && !params.To.IsZero() { - where := fmt.Sprintf("stored_at <= %s", fmt.Sprintf("$%d", len(args)+1)) args = append(args, params.To.UTC()) + where := fmt.Sprintf("stored_at <= %s", fmt.Sprintf("$%d", len(args))) and = append(and, where) } - // Action types + // Action types (issue, transfer or redeem) if len(params.ActionTypes) > 0 { - t := make([]interface{}, len(params.ActionTypes)) - for i, s := range params.ActionTypes { - t[i] = int(s) - } - add(&and, in(&args, "action_type", t)) + and = append(and, in(&args, "action_type", params.ActionTypes)) } // Specific transaction status if requested, defaults to all but Deleted if len(params.Statuses) > 0 { - t := make([]interface{}, len(params.Statuses)) - for i, s := range params.Statuses { - t[i] = s - } - add(&and, in(&args, "status", t)) + and = append(and, in(&args, "status", params.Statuses)) } // See QueryTransactionsParams for expected behavior. If only one of sender or // recipient is set, we return all transactions. If both are set, we do an OR. if params.SenderWallet != "" && params.RecipientWallet != "" { - where := fmt.Sprintf("(sender_eid = %s OR recipient_eid = %s)", fmt.Sprintf("$%d", len(args)+1), fmt.Sprintf("$%d", len(args)+2)) args = append(args, params.SenderWallet, params.RecipientWallet) + where := fmt.Sprintf("(sender_eid = %s OR recipient_eid = %s)", fmt.Sprintf("$%d", len(args)-1), fmt.Sprintf("$%d", len(args))) and = append(and, where) } if len(and) == 0 { return "", args } - where := fmt.Sprintf("WHERE %s", strings.Join(and, " AND ")) + where = fmt.Sprintf("WHERE %s", strings.Join(and, " AND ")) - return where, args + return } -func validationConditionsSql(params driver.QueryValidationRecordsParams) (string, []interface{}) { - args := make([]interface{}, 0) - and := make([]string, 0) +func validationConditionsSql(params driver.QueryValidationRecordsParams) (where string, args []any) { + and := []string{} // Timestamp from if params.From != nil && !params.From.IsZero() { - where := fmt.Sprintf("stored_at >= %s", fmt.Sprintf("$%d", len(args)+1)) args = append(args, params.From) + where := fmt.Sprintf("stored_at >= %s", fmt.Sprintf("$%d", len(args))) and = append(and, where) } // Timestamp to if params.To != nil && !params.To.IsZero() { - where := fmt.Sprintf("stored_at <= %s", fmt.Sprintf("$%d", len(args)+1)) args = append(args, params.To) + where := fmt.Sprintf("stored_at <= %s", fmt.Sprintf("$%d", len(args))) and = append(and, where) } // Specific transaction status if requested, defaults to all but Deleted if len(params.Statuses) > 0 { - t := make([]interface{}, len(params.Statuses)) - for i, s := range params.Statuses { - t[i] = int(s) - } - add(&and, in(&args, "status", t)) + and = append(and, in(&args, "status", params.Statuses)) } - - if len(and) == 0 { - return "", args + if len(and) > 0 { + where = fmt.Sprintf("WHERE %s", strings.Join(and, " AND ")) } - where := fmt.Sprintf("WHERE %s", strings.Join(and, " AND ")) - return where, args + return } -func movementConditionsSql(params driver.QueryMovementsParams) (string, []interface{}) { - args := make([]interface{}, 0) - and := make([]string, 0) +func movementConditionsSql(params driver.QueryMovementsParams) (where string, args []any) { + and := []string{} // Filter by enrollment id (any) - t := make([]interface{}, len(params.EnrollmentIDs)) - for i, s := range params.EnrollmentIDs { - t[i] = s + if len(params.EnrollmentIDs) > 0 { + and = append(and, in(&args, "enrollment_id", params.EnrollmentIDs)) } - add(&and, in(&args, "enrollment_id", t)) // Filter by token type (any) - t = make([]interface{}, len(params.TokenTypes)) - for i, s := range params.TokenTypes { - t[i] = s + if len(params.TokenTypes) > 0 { + and = append(and, in(&args, "token_type", params.TokenTypes)) } - add(&and, in(&args, "token_type", t)) // Specific transaction status if requested, defaults to all but Deleted if len(params.TxStatuses) > 0 { - statuses := make([]interface{}, len(params.TxStatuses)) - for i, s := range params.TxStatuses { - statuses[i] = s - } - add(&and, in(&args, "status", statuses)) + and = append(and, in(&args, "status", params.TxStatuses)) } else { and = append(and, fmt.Sprintf("status != %d", driver.Deleted)) } @@ -153,57 +140,66 @@ func movementConditionsSql(params driver.QueryMovementsParams) (string, []interf limit = fmt.Sprintf(" LIMIT %d", params.NumRecords) } - where := fmt.Sprintf("WHERE %s%s%s", strings.Join(and, " AND "), order, limit) + where = fmt.Sprintf("WHERE %s%s%s", strings.Join(and, " AND "), order, limit) - return where, args + return } -func certificationsQuerySql(ids []*token.ID) (string, []any, error) { - if len(ids) == 0 { - return "", nil, nil - } - if ids[0] == nil { - return "", nil, errors.Errorf("invalid token-id, cannot be nil") - } - var builder strings.Builder - builder.WriteString("token_id=$1") - var tokenIDs []any - tokenIDs = []any{fmt.Sprintf("%s%d", ids[0].TxId, ids[0].Index)} - for i := 1; i <= len(ids)-1; i++ { - if ids[i] == nil { - return "", nil, errors.Errorf("invalid token-id, cannot be nil") +// tokenQuerySql requires a join with the token ownership table if OwnerEnrollmentID is not empty +func tokenQuerySql(params driver.QueryTokenDetailsParams, tokenTable, ownerTable string) (where, join string, args []any) { + and := []string{"owner = true"} + if params.OwnerType != "" { + args = append(args, params.OwnerType) + and = append(and, fmt.Sprintf("owner_type = $%d", len(args))) + } + if params.OwnerEnrollmentID != "" { + args = append(args, params.OwnerEnrollmentID) + and = append(and, fmt.Sprintf("enrollment_id = $%d", len(args))) + } + + if params.TokenType != "" { + args = append(args, params.TokenType) + and = append(and, fmt.Sprintf("token_type = $%d", len(args))) + } + + if len(params.TransactionIDs) > 0 { + colTxID := "tx_id" + if len(tokenTable) > 0 { + colTxID = fmt.Sprintf("%s.%s", tokenTable, colTxID) } - builder.WriteString(" || ") - builder.WriteString(fmt.Sprintf("token_id=$%d", i+1)) - tokenIDs = append(tokenIDs, fmt.Sprintf("%s%d", ids[i].TxId, ids[i].Index)) + and = append(and, in(&args, colTxID, params.TransactionIDs)) + } + if ids := whereTokenIDsForJoin(tokenTable, &args, params.IDs); ids != "" { + and = append(and, ids) } - builder.WriteString("") - return builder.String(), tokenIDs, nil + if !params.IncludeDeleted { + and = append(and, "is_deleted = false") + } + + join = joinOnTokenID(tokenTable, ownerTable) + where = fmt.Sprintf("WHERE %s", strings.Join(and, " AND ")) + + return } -func tokenRequestConditionsSql(params driver.QueryTokenRequestsParams) (string, []interface{}) { - args := make([]interface{}, 0) +func tokenRequestConditionsSql(params driver.QueryTokenRequestsParams) (string, []any) { + args := make([]any, 0) and := make([]string, 0) + if len(params.Statuses) == 0 { + return "", args + } // Specific transaction status if requested, defaults to all but Deleted if len(params.Statuses) > 0 { - t := make([]interface{}, len(params.Statuses)) - for i, s := range params.Statuses { - t[i] = int(s) - } - add(&and, in(&args, "status", t)) - } - - if len(and) == 0 { - return "", args + and = append(and, in(&args, "status", params.Statuses)) } where := fmt.Sprintf("WHERE %s", strings.Join(and, " AND ")) return where, args } -func in(args *[]interface{}, field string, searchFor []interface{}) (where string) { +func in[T string | driver.TxStatus | driver.ActionType](args *[]any, field string, searchFor []T) (where string) { if len(searchFor) == 0 { return "" } @@ -221,31 +217,34 @@ func in(args *[]interface{}, field string, searchFor []interface{}) (where strin return fmt.Sprintf("(%s)", strings.Join(argnum, " OR ")) } -func whereTokenIDs(args *[]interface{}, ids []*token.ID) string { - switch len(ids) { - case 0: +func whereTokenIDsForJoin(tableName string, args *[]any, ids []*token.ID) (where string) { + if len(ids) == 0 { return "" - case 1: - *args = append(*args, ids[0].TxId, ids[0].Index) - l := len(*args) - return fmt.Sprintf("tx_id = %s AND idx = %s", fmt.Sprintf("$%d", l-1), fmt.Sprintf("$%d", l)) - default: - in := make([]string, len(ids)) - for i, id := range ids { - *args = append(*args, id.TxId, id.Index) - l := len(*args) - in[i] = fmt.Sprintf("($%d, $%d)", l-1, l) - } - return fmt.Sprintf("(tx_id, idx) IN ( %s )", strings.Join(in, ", ")) } -} -func add(and *[]string, clause string) { - if clause != "" { - *and = append(*and, clause) + colTxID := "tx_id" + colIdx := "idx" + if len(tableName) > 0 { + colTxID = fmt.Sprintf("%s.%s", tableName, colTxID) + colIdx = fmt.Sprintf("%s.%s", tableName, colIdx) } + + in := make([]string, len(ids)) + for i, id := range ids { + *args = append(*args, id.TxId, id.Index) + in[i] = fmt.Sprintf("($%d, $%d)", len(*args)-1, len(*args)) + } + return fmt.Sprintf("(%s, %s) IN ( %s )", colTxID, colIdx, strings.Join(in, ", ")) +} + +func whereTokenIDs(args *[]any, ids []*token.ID) (where string) { + return whereTokenIDsForJoin("", args, ids) +} + +func joinOnTxID(table, other string) string { + return fmt.Sprintf("LEFT JOIN %s ON %s.tx_id = %s.tx_id", other, table, other) } -func joinOnTxID(table, parent string) string { - return fmt.Sprintf("LEFT JOIN %s ON %s.tx_id = %s.tx_id", parent, table, parent) +func joinOnTokenID(table, other string) string { + return fmt.Sprintf("LEFT JOIN %s ON %s.tx_id = %s.tx_id AND %s.idx = %s.idx", other, table, other, table, other) } diff --git a/token/services/db/sql/querybuilder_test.go b/token/services/db/sql/querybuilder_test.go index ae5fa1238..0f035c5ea 100644 --- a/token/services/db/sql/querybuilder_test.go +++ b/token/services/db/sql/querybuilder_test.go @@ -12,7 +12,7 @@ import ( "time" "github.com/hyperledger-labs/fabric-token-sdk/token/services/db/driver" - token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/test-go/testify/assert" ) @@ -84,11 +84,31 @@ func TestTransactionSql(t *testing.T) { expectedSql: "WHERE stored_at >= $1 AND stored_at <= $2", expectedArgs: []interface{}{&lastYear, &now}, }, + { + name: "Sender OR recipient matches, specific tx", + params: driver.QueryTransactionsParams{ + SenderWallet: "alice", + RecipientWallet: "bob", + IDs: []string{"transactionID"}, + }, + expectedSql: "WHERE tbl.tx_id = $1 AND (sender_eid = $2 OR recipient_eid = $3)", + expectedArgs: []interface{}{"transactionID", "alice", "bob"}, + }, + { + name: "Sender OR recipient matches, specific tx ids", + params: driver.QueryTransactionsParams{ + SenderWallet: "alice", + RecipientWallet: "bob", + IDs: []string{"transactionID1", "transactionID2", "transactionID3"}, + }, + expectedSql: "WHERE (tbl.tx_id = $1 OR tbl.tx_id = $2 OR tbl.tx_id = $3) AND (sender_eid = $4 OR recipient_eid = $5)", + expectedArgs: []interface{}{"transactionID1", "transactionID2", "transactionID3", "alice", "bob"}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - actualSql, actualArgs := transactionsConditionsSql(tc.params) + actualSql, actualArgs := transactionsConditionsSql(tc.params, "tbl") assert.Equal(t, tc.expectedSql, actualSql) compareArgs(t, tc.expectedArgs, actualArgs) }) @@ -211,27 +231,117 @@ func TestMovementConditions(t *testing.T) { } } +func TestTokenSql(t *testing.T) { + testCases := []struct { + name string + params driver.QueryTokenDetailsParams + expectedArgs []interface{} + expectedSql string + }{ + { + name: "no filter", + params: driver.QueryTokenDetailsParams{}, + expectedSql: "WHERE owner = true AND is_deleted = false", + expectedArgs: []interface{}{}, + }, + { + name: "no filter with deleted", + params: driver.QueryTokenDetailsParams{ + IncludeDeleted: true, + }, + expectedSql: "WHERE owner = true", + expectedArgs: []interface{}{}, + }, + { + name: "owner unspent", + params: driver.QueryTokenDetailsParams{OwnerEnrollmentID: "me"}, + expectedSql: "WHERE owner = true AND enrollment_id = $1 AND is_deleted = false", + expectedArgs: []interface{}{"me"}, + }, + { + name: "owner with deleted", + params: driver.QueryTokenDetailsParams{ + OwnerEnrollmentID: "me", + IncludeDeleted: true, + }, + expectedSql: "WHERE owner = true AND enrollment_id = $1", + expectedArgs: []interface{}{"me"}, + }, + { + name: "owner and htlc with deleted", + params: driver.QueryTokenDetailsParams{ + OwnerEnrollmentID: "me", + OwnerType: "htlc", + IncludeDeleted: true, + }, + expectedSql: "WHERE owner = true AND owner_type = $1 AND enrollment_id = $2", + expectedArgs: []interface{}{"htlc", "me"}, + }, + { + name: "owner and type", + params: driver.QueryTokenDetailsParams{TokenType: "tok", OwnerEnrollmentID: "me"}, + expectedSql: "WHERE owner = true AND enrollment_id = $1 AND token_type = $2 AND is_deleted = false", + expectedArgs: []interface{}{"me", "tok"}, + }, + { + name: "owner and type and id", + params: driver.QueryTokenDetailsParams{ + TokenType: "tok", + OwnerEnrollmentID: "me", + IDs: []*token.ID{{TxId: "a", Index: 1}}, + }, + expectedSql: "WHERE owner = true AND enrollment_id = $1 AND token_type = $2 AND (tx_id, idx) IN ( ($3, $4) ) AND is_deleted = false", + expectedArgs: []interface{}{"me", "tok", "a", 1}, + }, + { + name: "type and ids", + params: driver.QueryTokenDetailsParams{ + TokenType: "tok", + IDs: []*token.ID{{TxId: "a", Index: 1}, {TxId: "b", Index: 2}}, + IncludeDeleted: true, + }, + expectedSql: "WHERE owner = true AND token_type = $1 AND (tx_id, idx) IN ( ($2, $3), ($4, $5) )", + expectedArgs: []interface{}{"tok", "a", uint64(1), "b", uint64(2)}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualSql, _, actualArgs := tokenQuerySql(tc.params, "", "") + assert.Equal(t, tc.expectedSql, actualSql, tc.name) + compareArgs(t, tc.expectedArgs, actualArgs) + }) + } + // with join + where, join, args := tokenQuerySql(driver.QueryTokenDetailsParams{ + IDs: []*token.ID{{TxId: "a", Index: 1}}, + OwnerEnrollmentID: "me", + }, "A", "B") + assert.Equal(t, "WHERE owner = true AND enrollment_id = $1 AND (A.tx_id, A.idx) IN ( ($2, $3) ) AND is_deleted = false", where, "join") + assert.Equal(t, "LEFT JOIN B ON A.tx_id = B.tx_id AND A.idx = B.idx", join, "join") + assert.Len(t, args, 3) +} + func TestIn(t *testing.T) { // 0 - args := make([]interface{}, 0) - w := in(&args, "enrollment_id", []interface{}{}) + args := make([]any, 0) + w := in(&args, "enrollment_id", []string{}) assert.Equal(t, "", w) - assert.Equal(t, []interface{}{}, args) + assert.Equal(t, []any{}, args) // 1 - args = make([]interface{}, 0) - w = in(&args, "enrollment_id", []interface{}{"eid1"}) + args = make([]any, 0) + w = in(&args, "enrollment_id", []string{"eid1"}) assert.Equal(t, "enrollment_id = $1", w) - assert.Equal(t, []interface{}{"eid1"}, args) + assert.Equal(t, []any{"eid1"}, args) // 3 - args = make([]interface{}, 0) - w = in(&args, "enrollment_id", []interface{}{"eid1", "eid2", "eid3"}) + args = make([]any, 0) + w = in(&args, "enrollment_id", []string{"eid1", "eid2", "eid3"}) assert.Equal(t, "(enrollment_id = $1 OR enrollment_id = $2 OR enrollment_id = $3)", w) - assert.Equal(t, []interface{}{"eid1", "eid2", "eid3"}, args) + assert.Equal(t, []any{"eid1", "eid2", "eid3"}, args) } -func compareArgs(t *testing.T, expected, actual []interface{}) { +func compareArgs(t *testing.T, expected, actual []any) { assert.Len(t, actual, len(expected)) // assert.Equal(t, tc.expectedArgs, actualArgs) @@ -242,37 +352,15 @@ func compareArgs(t *testing.T, expected, actual []interface{}) { act, _ := actual[i].(time.Time) assert.True(t, exp.Equal(act), fmt.Sprintf("timestamps not equal: %v != %v", exp, act)) default: - assert.Equal(t, expected[i], actual[i]) + assert.EqualValues(t, expected[i], actual[i]) } } } -func TestCertificationsQuerySql(t *testing.T) { - ids := []*token2.ID{ - { - TxId: "pineapple", - Index: 1, - }, - { - TxId: "banana", - Index: 2, - }, - } - conditions, idStrs, err := certificationsQuerySql(ids) - assert.NoError(t, err) - assert.Equal(t, conditions, "token_id=$1 || token_id=$2") - assert.Len(t, idStrs, len(ids)) - for i := 0; i < len(ids); i++ { - assert.Equal(t, fmt.Sprintf("%s%d", ids[i].TxId, ids[i].Index), idStrs[i]) - } - - conditions, idStrs, err = certificationsQuerySql(nil) - assert.NoError(t, err) - assert.Equal(t, "", conditions) - assert.Nil(t, idStrs) -} - func TestJoin(t *testing.T) { j := joinOnTxID("t1", "t2") assert.Equal(t, "LEFT JOIN t2 ON t1.tx_id = t2.tx_id", j) + + j = joinOnTokenID("t1", "t2") + assert.Equal(t, "LEFT JOIN t2 ON t1.tx_id = t2.tx_id AND t1.idx = t2.idx", j) } diff --git a/token/services/db/sql/tokens.go b/token/services/db/sql/tokens.go index 6a878fee0..5b56b2c7f 100644 --- a/token/services/db/sql/tokens.go +++ b/token/services/db/sql/tokens.go @@ -10,6 +10,7 @@ import ( "database/sql" "fmt" "runtime/debug" + "strings" "time" tdriver "github.com/hyperledger-labs/fabric-token-sdk/token/driver" @@ -110,51 +111,28 @@ func (db *TokenDB) IsMine(txID string, index uint64) (bool, error) { // UnspentTokensIterator returns an iterator over all unspent tokens func (db *TokenDB) UnspentTokensIterator() (tdriver.UnspentTokensIterator, error) { - var uti UnspentTokensIterator - - query := fmt.Sprintf("SELECT tx_id, idx, owner_raw, token_type, quantity FROM %s WHERE is_deleted = false AND owner = true", db.table.Tokens) - logger.Debug(query) - rows, err := db.db.Query(query) - uti.txs = rows - return &uti, err + return db.UnspentTokensIteratorBy("", "") } // UnspentTokensIteratorBy returns an iterator of unspent tokens owned by the passed id and whose type is the passed on. // The token type can be empty. In that case, tokens of any type are returned. func (db *TokenDB) UnspentTokensIteratorBy(ownerEID, typ string) (tdriver.UnspentTokensIterator, error) { - var uti UnspentTokensIterator - - var args []interface{} - if ownerEID != "" { - args = append(args, ownerEID) - } - if typ != "" { - args = append(args, typ) - } - query := fmt.Sprintf("SELECT %s.tx_id, %s.idx, owner_raw, token_type, quantity FROM %s INNER JOIN %s ON %s.tx_id = %s.tx_id AND %s.idx = %s.idx AND %s.is_deleted = false AND %s.owner = true ", - db.table.Tokens, db.table.Tokens, // select - db.table.Tokens, // from - db.table.Ownership, // inner join - db.table.Tokens, db.table.Ownership, // .txid - db.table.Tokens, db.table.Ownership, // .idx - db.table.Tokens, // Unspent - db.table.Tokens, // owner token - ) - if ownerEID != "" { - query += " AND enrollment_id = $1" - } - if typ != "" { - query += fmt.Sprintf(" AND token_type = $%d", len(args)) - } + where, join, args := tokenQuerySql(driver.QueryTokenDetailsParams{ + OwnerEnrollmentID: ownerEID, + TokenType: typ, + }, db.table.Tokens, db.table.Ownership) + query := fmt.Sprintf("SELECT %s.tx_id, %s.idx, owner_raw, token_type, quantity FROM %s %s %s", + db.table.Tokens, db.table.Tokens, db.table.Tokens, join, where) + logger.Debug(query, args) rows, err := db.db.Query(query, args...) - uti.txs = rows - return &uti, err + + return &UnspentTokensIterator{txs: rows}, err } // ListUnspentTokensBy returns the list of unspent tokens, filtered by owner and token type func (db *TokenDB) ListUnspentTokensBy(ownerEID, typ string) (*token.UnspentTokens, error) { - logger.Debugf("List unspent token...") + logger.Debugf("list unspent token by [%s,%s]", ownerEID, typ) it, err := db.UnspentTokensIteratorBy(ownerEID, typ) if err != nil { return nil, err @@ -177,7 +155,7 @@ func (db *TokenDB) ListUnspentTokensBy(ownerEID, typ string) (*token.UnspentToke // ListUnspentTokens returns the list of unspent tokens func (db *TokenDB) ListUnspentTokens() (*token.UnspentTokens, error) { - logger.Debugf("List unspent token...") + logger.Debugf("list unspent tokens...") it, err := db.UnspentTokensIterator() if err != nil { return nil, err @@ -505,21 +483,57 @@ func (db *TokenDB) GetTokens(inputs ...*token.ID) ([]string, []*token.Token, err return ids, tokens, nil } +// QueryTokensDetails returns details about owned tokens, regardless if they have been spent or not. +// Filters work cumulatively and may be left empty. If a token is owned by two enrollmentIDs and there +// is no filter on enrollmentID, the token will be returned twice (once for each owner). +func (db *TokenDB) QueryTokenDetails(params driver.QueryTokenDetailsParams) ([]driver.TokenDetails, error) { + where, join, args := tokenQuerySql(params, db.table.Tokens, db.table.Ownership) + + query := fmt.Sprintf("SELECT %s.tx_id, %s.idx, owner_identity, owner_type, enrollment_id, token_type, amount, is_deleted, spent_by, stored_at FROM %s %s %s", + db.table.Tokens, db.table.Tokens, db.table.Tokens, join, where) + logger.Debug(query, args) + rows, err := db.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + deets := []driver.TokenDetails{} + for rows.Next() { + td := driver.TokenDetails{} + if err := rows.Scan( + &td.TxID, + &td.Index, + &td.OwnerIdentity, + &td.OwnerType, + &td.OwnerEnrollment, + &td.Type, + &td.Amount, + &td.IsSpent, + &td.SpentBy, + &td.StoredAt, + ); err != nil { + return deets, err + } + deets = append(deets, td) + } + logger.Debugf("found [%d] tokens", len(deets)) + if err = rows.Err(); err != nil { + return deets, err + } + return deets, nil +} + // WhoDeletedTokens returns information about which transaction deleted the passed tokens. // The bool array is an indicator used to tell if the token at a given position has been deleted or not func (db *TokenDB) WhoDeletedTokens(inputs ...*token.ID) ([]string, []bool, error) { - logger.Debugf("search first over token table [%s]...", inputs) - return db.whoDeleteTokens(db.table.Tokens, inputs...) -} - -func (db *TokenDB) whoDeleteTokens(table string, inputs ...*token.ID) ([]string, []bool, error) { if len(inputs) == 0 { return []string{}, []bool{}, nil } - args := make([]interface{}, 0) + args := []any{} where := whereTokenIDs(&args, inputs) - query := fmt.Sprintf("SELECT tx_id, idx, spent_by, is_deleted FROM %s WHERE %s", table, where) + query := fmt.Sprintf("SELECT tx_id, idx, spent_by, is_deleted FROM %s WHERE %s", db.table.Tokens, where) logger.Debug(query, args) rows, err := db.db.Query(query, args...) if err != nil { @@ -567,7 +581,6 @@ func (db *TokenDB) whoDeleteTokens(table string, inputs ...*token.ID) ([]string, panic("programming error: should not reach this point") } return spentBy, isSpent, nil - } func (db *TokenDB) StorePublicParams(raw []byte) error { @@ -597,7 +610,7 @@ func (db *TokenDB) PublicParams() ([]byte, error) { func (db *TokenDB) StoreCertifications(certifications map[*token.ID][]byte) (err error) { now := time.Now().UTC() - query := fmt.Sprintf("INSERT INTO %s (token_id, tx_id, idx, certification, stored_at) VALUES ($1, $2, $3, $4, $5)", db.table.Certifications) + query := fmt.Sprintf("INSERT INTO %s (tx_id, idx, certification, stored_at) VALUES ($1, $2, $3, $4)", db.table.Certifications) tx, err := db.db.Begin() if err != nil { @@ -610,18 +623,18 @@ func (db *TokenDB) StoreCertifications(certifications map[*token.ID][]byte) (err } } }() + for tokenID, certification := range certifications { if tokenID == nil { return errors.Errorf("invalid token-id, cannot be nil") } - tokenIDStr := fmt.Sprintf("%s%d", tokenID.TxId, tokenID.Index) - logger.Debug(query, tokenIDStr, fmt.Sprintf("(%d bytes)", len(certification)), now) - if _, err = tx.Exec(query, tokenIDStr, tokenID.TxId, tokenID.Index, certification, now); err != nil { - return errors.Wrapf(err, "failed to execute") + logger.Debug(query, fmt.Sprintf("(%d bytes)", len(certification)), now) + if _, err = tx.Exec(query, tokenID.TxId, tokenID.Index, certification, now); err != nil { + return tokenDBError(err) } } if err = tx.Commit(); err != nil { - return errors.Wrap(err, "failed committing status update") + return errors.Wrap(err, "failed committing certifications") } return } @@ -630,40 +643,37 @@ func (db *TokenDB) ExistsCertification(tokenID *token.ID) bool { if tokenID == nil { return false } - tokenIDStr := fmt.Sprintf("%s%d", tokenID.TxId, tokenID.Index) - query := fmt.Sprintf("SELECT certification FROM %s WHERE token_id=$1;", db.table.Certifications) - logger.Debug(query, tokenIDStr) + args := []any{} + where := whereTokenIDs(&args, []*token.ID{tokenID}) + + query := fmt.Sprintf("SELECT certification FROM %s WHERE %s", db.table.Certifications, where) + logger.Debug(query, args) + row := db.db.QueryRow(query, args...) - row := db.db.QueryRow(query, tokenIDStr) var certification []byte if err := row.Scan(&certification); err != nil { if errors.Is(err, sql.ErrNoRows) { return false } - logger.Warnf("tried to check certification existence for token id %s, err %s", tokenIDStr, err) + logger.Warnf("tried to check certification existence for token id %s, err %s", tokenID, err) return false } result := len(certification) != 0 if !result { - logger.Warnf("tried to check certification existence for token id %s, got an empty certification", tokenIDStr) + logger.Warnf("tried to check certification existence for token id %s, got an empty certification", tokenID) } return result } func (db *TokenDB) GetCertifications(ids []*token.ID) ([][]byte, error) { if len(ids) == 0 { - // nothing to do here return nil, nil } + args := []any{} + where := whereTokenIDs(&args, ids) + query := fmt.Sprintf("SELECT tx_id, idx, certification FROM %s WHERE %s ", db.table.Certifications, where) - // build query - conditions, tokenIDs, err := certificationsQuerySql(ids) - if err != nil { - return nil, err - } - query := fmt.Sprintf("SELECT tx_id, idx, certification FROM %s WHERE ", db.table.Certifications) + conditions - - rows, err := db.db.Query(query, tokenIDs...) + rows, err := db.db.Query(query, args...) if err != nil { return nil, errors.Wrapf(err, "failed to query") } @@ -706,6 +716,8 @@ func (db *TokenDB) GetSchema() string { quantity TEXT NOT NULL, issuer_raw BYTEA, owner_raw BYTEA NOT NULL, + owner_type TEXT NOT NULL, + owner_identity BYTEA NOT NULL, ledger BYTEA NOT NULL, ledger_metadata BYTEA NOT NULL, stored_at TIMESTAMP NOT NULL, @@ -725,7 +737,8 @@ func (db *TokenDB) GetSchema() string { tx_id TEXT NOT NULL, idx INT NOT NULL, enrollment_id TEXT NOT NULL, - PRIMARY KEY (tx_id, idx, enrollment_id) + PRIMARY KEY (tx_id, idx, enrollment_id), + FOREIGN KEY (tx_id, idx) REFERENCES %s ); -- Public Parameters @@ -736,22 +749,20 @@ func (db *TokenDB) GetSchema() string { -- Certifications CREATE TABLE IF NOT EXISTS %s ( - token_id TEXT NOT NULL PRIMARY KEY, tx_id TEXT NOT NULL, idx INT NOT NULL, certification BYTEA NOT NULL, - stored_at TIMESTAMP NOT NULL + stored_at TIMESTAMP NOT NULL, + PRIMARY KEY (tx_id, idx), + FOREIGN KEY (tx_id, idx) REFERENCES %s ); - CREATE INDEX IF NOT EXISTS exists_%s ON %s ( token_id ); `, + db.table.Tokens, db.table.Tokens, db.table.Tokens, db.table.Tokens, db.table.Tokens, - db.table.Tokens, - db.table.Ownership, + db.table.Ownership, db.table.Tokens, db.table.PublicParams, - db.table.Certifications, - db.table.Certifications, - db.table.Certifications, + db.table.Certifications, db.table.Tokens, ) } @@ -777,50 +788,57 @@ func (t *TokenTransaction) TransactionExists(id string) (bool, error) { logger.Debug(query, id) row := t.tx.QueryRow(query, id) - var certification []byte - if err := row.Scan(&certification); err != nil { - if errors.Is(err, sql.ErrNoRows) { + var found string + if err := row.Scan(&found); err != nil { + if err == sql.ErrNoRows { return false, nil } logger.Warnf("tried to check transaction existence for id %s, err %s", id, err) return false, err } - result := len(certification) != 0 - if !result { - logger.Warnf("tried to check transaction existence for id %s, got nothing", id) - } - return result, nil - + return true, nil } -func (t *TokenTransaction) GetToken(txID string, index uint64, includeDeleted bool) (*token.Token, error) { - args := make([]interface{}, 0) - tokenIDs := []*token.ID{{TxId: txID, Index: index}} - where := whereTokenIDs(&args, tokenIDs) - var del string - if !includeDeleted { - del = "AND is_deleted = false" - } +func (t *TokenTransaction) GetToken(txID string, index uint64, includeDeleted bool) (*token.Token, []string, error) { + where, join, args := tokenQuerySql(driver.QueryTokenDetailsParams{ + IDs: []*token.ID{{TxId: txID, Index: index}}, + IncludeDeleted: includeDeleted, + }, t.db.table.Tokens, t.db.table.Ownership) - query := fmt.Sprintf("SELECT owner_raw, token_type, quantity FROM %s WHERE %s AND owner = true %s", t.db.table.Tokens, where, del) + query := fmt.Sprintf("SELECT owner_raw, token_type, quantity, enrollment_id FROM %s %s %s", t.db.table.Tokens, join, where) logger.Debug(query, args) - row := t.tx.QueryRow(query, args...) - var tokenOwner []byte + rows, err := t.tx.Query(query, args...) + if err != nil { + return nil, nil, err + } + defer rows.Close() + + var raw []byte var tokenType string var quantity string - if err := row.Scan(&tokenOwner, &tokenType, &quantity); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil + owners := []string{} + for rows.Next() { + var owner string + if err := rows.Scan(&raw, &tokenType, &quantity, &owner); err != nil { + return nil, owners, err + } + if len(owner) > 0 { + owners = append(owners, owner) } - return nil, err + } + if rows.Err() != nil { + return nil, nil, rows.Err() + } + if len(raw) == 0 { + return nil, owners, nil } return &token.Token{ Owner: &token.Owner{ - Raw: tokenOwner, + Raw: raw, }, Type: tokenType, Quantity: quantity, - }, nil + }, owners, nil } func (t *TokenTransaction) OwnersOf(txID string, index uint64) ([]string, error) { @@ -869,9 +887,39 @@ func (t *TokenTransaction) StoreToken(tr driver.TokenRecord, owners []string) er // Store token now := time.Now().UTC() - query := fmt.Sprintf("INSERT INTO %s (tx_id, idx, issuer_raw, owner_raw, ledger, ledger_metadata, token_type, quantity, amount, stored_at, owner, auditor, issuer) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", t.db.table.Tokens) - logger.Debug(query, tr.TxID, tr.Index, len(tr.IssuerRaw), len(tr.OwnerRaw), len(tr.Ledger), len(tr.LedgerMetadata), tr.Type, tr.Quantity, tr.Amount, now, tr.Owner, tr.Auditor, tr.Issuer) - if _, err := t.tx.Exec(query, tr.TxID, tr.Index, tr.IssuerRaw, tr.OwnerRaw, tr.Ledger, tr.LedgerMetadata, tr.Type, tr.Quantity, tr.Amount, now, tr.Owner, tr.Auditor, tr.Issuer); err != nil { + query := fmt.Sprintf("INSERT INTO %s (tx_id, idx, issuer_raw, owner_raw, owner_type, owner_identity, ledger, ledger_metadata, token_type, quantity, amount, stored_at, owner, auditor, issuer) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)", t.db.table.Tokens) + logger.Debug(query, + tr.TxID, + tr.Index, + len(tr.IssuerRaw), + len(tr.OwnerRaw), + tr.OwnerType, + len(tr.OwnerIdentity), + len(tr.Ledger), + len(tr.LedgerMetadata), + tr.Type, + tr.Quantity, + tr.Amount, + now, + tr.Owner, + tr.Auditor, + tr.Issuer) + if _, err := t.tx.Exec(query, + tr.TxID, + tr.Index, + tr.IssuerRaw, + tr.OwnerRaw, + tr.OwnerType, + tr.OwnerIdentity, + tr.Ledger, + tr.LedgerMetadata, + tr.Type, + tr.Quantity, + tr.Amount, + now, + tr.Owner, + tr.Auditor, + tr.Issuer); err != nil { logger.Errorf("error storing token [%s] in table [%s]: [%s][%s]", tr.TxID, t.db.table.Tokens, err, string(debug.Stack())) return errors.Wrapf(err, "error storing token [%s] in table [%s]", tr.TxID, t.db.table.Tokens) } @@ -932,3 +980,15 @@ func (u *UnspentTokensIterator) Next() (*token.UnspentToken, error) { Quantity: quantity, }, err } + +func tokenDBError(err error) error { + if err == nil { + return nil + } + logger.Error(err) + e := strings.ToLower(err.Error()) + if strings.Contains(e, "foreign key constraint") { + return driver.ErrTokenDoesNotExist + } + return err +} diff --git a/token/services/db/sql/tokens_test.go b/token/services/db/sql/tokens_test.go index 40bd30ae9..debf303ab 100644 --- a/token/services/db/sql/tokens_test.go +++ b/token/services/db/sql/tokens_test.go @@ -29,7 +29,7 @@ func initTokenDB(driverName, dataSourceName, tablePrefix string, maxOpenConns in func TestTokensSqlite(t *testing.T) { tempDir := t.TempDir() for _, c := range TokensCases { - db, err := initTokenDB("sqlite", fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)", path.Join(tempDir, "db.sqlite")), c.Name, 10) + db, err := initTokenDB("sqlite", fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)", path.Join(tempDir, "db.sqlite")), c.Name, 10) if err != nil { t.Fatal(err) } @@ -42,7 +42,7 @@ func TestTokensSqlite(t *testing.T) { func TestTokensSqliteMemory(t *testing.T) { for _, c := range TokensCases { - db, err := initTokenDB("sqlite", "file:tmp?_pragma=busy_timeout(5000)&mode=memory&cache=shared", c.Name, 10) + db, err := initTokenDB("sqlite", "file:tmp?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&mode=memory&cache=shared", c.Name, 10) if err != nil { t.Fatal(err) } @@ -94,6 +94,8 @@ func TTransaction(t *testing.T, db *TokenDB) { Index: 0, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x02", @@ -110,14 +112,18 @@ func TTransaction(t *testing.T, db *TokenDB) { if err != nil { t.Fatal(err) } - tok, err := tx.GetToken("tx1", 0, false) - assert.NoError(t, err) + tok, owners, err := tx.GetToken("tx1", 0, false) + assert.NoError(t, err, "get token") assert.Equal(t, "0x02", tok.Quantity) + assert.Equal(t, []string{"alice"}, owners) + assert.NoError(t, tx.Delete("tx1", 0, "me")) - tok, err = tx.GetToken("tx1", 0, false) + tok, owners, err = tx.GetToken("tx1", 0, false) assert.NoError(t, err) assert.Nil(t, tok) - tok, err = tx.GetToken("tx1", 0, true) // include deleted + assert.Len(t, owners, 0) + + tok, _, err = tx.GetToken("tx1", 0, true) // include deleted assert.NoError(t, err) assert.Equal(t, "0x02", tok.Quantity) assert.NoError(t, tx.Rollback()) @@ -126,10 +132,11 @@ func TTransaction(t *testing.T, db *TokenDB) { if err != nil { t.Fatal(err) } - tok, err = tx.GetToken("tx1", 0, false) + tok, owners, err = tx.GetToken("tx1", 0, false) assert.NoError(t, err) assert.NotNil(t, tok) assert.Equal(t, "0x02", tok.Quantity) + assert.Equal(t, []string{"alice"}, owners) assert.NoError(t, tx.Delete("tx1", 0, "me")) assert.NoError(t, tx.Commit()) @@ -137,41 +144,45 @@ func TTransaction(t *testing.T, db *TokenDB) { if err != nil { t.Fatal(err) } - tok, err = tx.GetToken("tx1", 0, false) + tok, owners, err = tx.GetToken("tx1", 0, false) assert.NoError(t, err) assert.Nil(t, tok) + assert.Equal(t, []string{}, owners) assert.NoError(t, tx.Commit()) } func TSaveAndGetToken(t *testing.T, db *TokenDB) { for i := 0; i < 20; i++ { - owners := []string{"alice"} tr := driver.TokenRecord{ TxID: fmt.Sprintf("tx%d", i), Index: 0, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x02", Type: "TST", - Amount: 0, + Amount: 2, Owner: true, Auditor: false, Issuer: false, } - assert.NoError(t, db.StoreToken(tr, owners)) + assert.NoError(t, db.StoreToken(tr, []string{"alice"})) } tr := driver.TokenRecord{ TxID: fmt.Sprintf("tx%d", 100), Index: 0, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x02", Type: "TST", - Amount: 0, + Amount: 2, Owner: true, Auditor: false, Issuer: false, @@ -183,11 +194,13 @@ func TSaveAndGetToken(t *testing.T, db *TokenDB) { Index: 1, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x02", Type: "TST", - Amount: 0, + Amount: 2, Owner: true, Auditor: false, Issuer: false, @@ -199,11 +212,13 @@ func TSaveAndGetToken(t *testing.T, db *TokenDB) { Index: 0, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x02", Type: "ABC", - Amount: 0, + Amount: 2, Owner: true, Auditor: false, Issuer: false, @@ -212,10 +227,9 @@ func TSaveAndGetToken(t *testing.T, db *TokenDB) { tok, err := db.ListUnspentTokens() assert.NoError(t, err) - assert.Len(t, tok.Tokens, 23, "unspentTokensIterator: expected all tokens to be returned") - assert.Equal(t, 23, tok.Count()) - assert.Equal(t, "46", tok.Sum(64).Decimal(), "expect sum to be 2*23") - assert.Len(t, tok.ByType("TST").Tokens, 22, "expect filter on type to work") + assert.Len(t, tok.Tokens, 24, "unspentTokensIterator: expected all tokens to be returned (2 for the one owned by alice and bob)") + assert.Equal(t, "48", tok.Sum(64).Decimal(), "expect sum to be 2*22") + assert.Len(t, tok.ByType("TST").Tokens, 23, "expect filter on type to work") for _, token := range tok.Tokens { assert.NotNil(t, token.Owner, "expected owner to not be nil") assert.NotEmpty(t, token.Owner.Raw, "expected owner raw to not be empty") @@ -265,6 +279,8 @@ func TDeleteAndMine(t *testing.T, db *TokenDB) { Index: 0, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x01", @@ -280,6 +296,8 @@ func TDeleteAndMine(t *testing.T, db *TokenDB) { Index: 1, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x01", @@ -295,6 +313,8 @@ func TDeleteAndMine(t *testing.T, db *TokenDB) { Index: 0, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x01", @@ -337,6 +357,8 @@ func TListAuditTokens(t *testing.T, db *TokenDB) { TxID: "tx101", Index: 0, OwnerRaw: []byte{1, 2}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x01", @@ -351,6 +373,8 @@ func TListAuditTokens(t *testing.T, db *TokenDB) { TxID: "tx101", Index: 1, OwnerRaw: []byte{3, 4}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x02", @@ -365,6 +389,8 @@ func TListAuditTokens(t *testing.T, db *TokenDB) { TxID: "tx102", Index: 0, OwnerRaw: []byte{5, 6}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x03", @@ -400,6 +426,8 @@ func TListIssuedTokens(t *testing.T, db *TokenDB) { TxID: "tx101", Index: 0, OwnerRaw: []byte{1, 2}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, IssuerRaw: []byte{11, 12}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, @@ -415,6 +443,8 @@ func TListIssuedTokens(t *testing.T, db *TokenDB) { TxID: "tx101", Index: 1, OwnerRaw: []byte{3, 4}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, IssuerRaw: []byte{13, 14}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, @@ -430,6 +460,8 @@ func TListIssuedTokens(t *testing.T, db *TokenDB) { TxID: "tx102", Index: 0, OwnerRaw: []byte{5, 6}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, IssuerRaw: []byte{15, 16}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, @@ -477,6 +509,8 @@ func TGetTokenInfos(t *testing.T, db *TokenDB) { Index: 0, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("tx101l"), LedgerMetadata: []byte("tx101"), Quantity: "0x01", @@ -492,6 +526,8 @@ func TGetTokenInfos(t *testing.T, db *TokenDB) { Index: 0, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("tx102l"), LedgerMetadata: []byte("tx102"), Quantity: "0x01", @@ -507,6 +543,8 @@ func TGetTokenInfos(t *testing.T, db *TokenDB) { Index: 1, IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("tx102l"), LedgerMetadata: []byte("tx102"), Quantity: "0x01", @@ -572,46 +610,40 @@ func TDeleteMultiple(t *testing.T, db *TokenDB) { tr := driver.TokenRecord{ TxID: "tx101", Index: 0, - IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x01", Type: "ABC", - Amount: 0, Owner: true, - Auditor: false, - Issuer: false, } assert.NoError(t, db.StoreToken(tr, []string{"alice"})) tr = driver.TokenRecord{ TxID: "tx101", Index: 1, - IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x01", Type: "ABC", - Amount: 0, Owner: true, - Auditor: false, - Issuer: false, } assert.NoError(t, db.StoreToken(tr, []string{"bob"})) tr = driver.TokenRecord{ TxID: "tx102", Index: 0, - IssuerRaw: []byte{}, OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Quantity: "0x01", Type: "ABC", - Amount: 0, Owner: true, - Auditor: false, - Issuer: false, } assert.NoError(t, db.StoreToken(tr, []string{"alice"})) assert.NoError(t, db.DeleteTokens("", &token.ID{TxId: "tx101", Index: 0}, &token.ID{TxId: "tx102", Index: 0})) @@ -661,6 +693,22 @@ func TCertification(t *testing.T, db *TokenDB) { TxId: fmt.Sprintf("tx_%d", i), Index: 0, } + err := db.StoreToken(driver.TokenRecord{ + TxID: tokenID.TxId, + Index: tokenID.Index, + OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, + Quantity: "0x01", + Ledger: []byte("ledger"), + LedgerMetadata: []byte{}, + Type: "ABC", + Owner: true, + }, []string{"alice"}) + if err != nil { + t.Error(err) + } + assert.NoError(t, db.StoreCertifications(map[*token.ID][]byte{ tokenID: []byte(fmt.Sprintf("certification_%d", i)), })) @@ -700,9 +748,10 @@ func TCertification(t *testing.T, db *TokenDB) { assert.Empty(t, certifications) // store an empty certification and check that an error is returned - assert.NoError(t, db.StoreCertifications(map[*token.ID][]byte{ + err = db.StoreCertifications(map[*token.ID][]byte{ tokenID: {}, - })) + }) + assert.Error(t, err) certifications, err = db.GetCertifications([]*token.ID{tokenID}) assert.Error(t, err) assert.Empty(t, certifications) diff --git a/token/services/db/sql/transactions.go b/token/services/db/sql/transactions.go index a5c41d5a3..35637aca2 100644 --- a/token/services/db/sql/transactions.go +++ b/token/services/db/sql/transactions.go @@ -117,10 +117,10 @@ func (db *TransactionDB) QueryMovements(params driver.QueryMovementsParams) (res } func (db *TransactionDB) QueryTransactions(params driver.QueryTransactionsParams) (driver.TransactionIterator, error) { - conditions, args := transactionsConditionsSql(params) + conditions, args := transactionsConditionsSql(params, db.table.Transactions) query := fmt.Sprintf( - "SELECT %s.tx_id, action_type, sender_eid, recipient_eid, token_type, amount, %s.status, stored_at FROM %s %s %s", - db.table.Transactions, db.table.Requests, + "SELECT %s.tx_id, action_type, sender_eid, recipient_eid, token_type, amount, %s.status, %s.application_metadata, stored_at FROM %s %s %s", + db.table.Transactions, db.table.Requests, db.table.Requests, db.table.Transactions, joinOnTxID(db.table.Transactions, db.table.Requests), conditions) logger.Debug(query, args) @@ -151,8 +151,8 @@ func (db *TransactionDB) GetStatus(txID string) (driver.TxStatus, string, error) func (db *TransactionDB) QueryValidations(params driver.QueryValidationRecordsParams) (driver.ValidationRecordsIterator, error) { conditions, args := validationConditionsSql(params) - query := fmt.Sprintf("SELECT %s.tx_id, %s.request, metadata, %s.status, stored_at FROM %s %s %s", - db.table.Validations, db.table.Requests, db.table.Requests, + query := fmt.Sprintf("SELECT %s.tx_id, %s.request, metadata, %s.status, %s.stored_at FROM %s %s %s", + db.table.Validations, db.table.Requests, db.table.Requests, db.table.Validations, db.table.Validations, joinOnTxID(db.table.Validations, db.table.Requests), conditions) logger.Debug(query, args) @@ -188,7 +188,7 @@ func (db *TransactionDB) AddTransactionEndorsementAck(txID string, endorser view return errors.Wrapf(err, "error generating uuid") } if _, err = db.db.Exec(query, id, txID, endorser, sigma, now); err != nil { - return dbError(err) + return ttxDBError(err) } return } @@ -256,7 +256,8 @@ func (db *TransactionDB) GetSchema() string { tx_id TEXT NOT NULL PRIMARY KEY, request BYTEA NOT NULL, status INT NOT NULL, - status_message TEXT NOT NULL + status_message TEXT NOT NULL, + application_metadata JSONB NOT NULL ); -- transactions @@ -336,6 +337,7 @@ func (t *TransactionIterator) Next() (*driver.TransactionRecord, error) { var actionType int var amount int64 var status int + var metadata []byte // tx_id, action_type, sender_eid, recipient_eid, token_type, amount, status, stored_at err := t.txs.Scan( &r.TxID, @@ -345,8 +347,13 @@ func (t *TransactionIterator) Next() (*driver.TransactionRecord, error) { &r.TokenType, &amount, &status, + &metadata, &r.Timestamp, ) + if err := unmarshal(metadata, &r.ApplicationMetadata); err != nil { + logger.Errorf("error unmarshaling application metadata: %v", metadata) + return &r, errors.New("error umarshaling application metadata") + } r.ActionType = driver.ActionType(actionType) r.Amount = big.NewInt(amount) @@ -494,19 +501,27 @@ func (w *AtomicWrite) AddTransaction(r *driver.TransactionRecord) error { logger.Debug(query, args) _, err = w.txn.Exec(query, args...) - return dbError(err) + return ttxDBError(err) } -func (w *AtomicWrite) AddTokenRequest(txID string, tr []byte) error { +func (w *AtomicWrite) AddTokenRequest(txID string, tr []byte, applicationMetadata map[string][]byte) error { logger.Debugf("adding token request [%s]", txID) if w.txn == nil { return errors.New("no db transaction in progress") } - query := fmt.Sprintf("INSERT INTO %s (tx_id, request, status, status_message) VALUES ($1, $2, $3, $4)", w.db.table.Requests) - logger.Debug(query, txID, fmt.Sprintf("(%d bytes)", len(tr))) + if applicationMetadata == nil { + applicationMetadata = make(map[string][]byte) + } + j, err := marshal(applicationMetadata) + if err != nil { + return errors.New("error marshaling application metadata") + } + + query := fmt.Sprintf("INSERT INTO %s (tx_id, request, status, status_message, application_metadata) VALUES ($1, $2, $3, $4, $5)", w.db.table.Requests) + logger.Debug(query, txID, fmt.Sprintf("(%d bytes)", len(tr)), len(applicationMetadata)) - _, err := w.txn.Exec(query, txID, tr, driver.Pending, "") - return dbError(err) + _, err = w.txn.Exec(query, txID, tr, driver.Pending, "", j) + return ttxDBError(err) } func (w *AtomicWrite) AddMovement(r *driver.MovementRecord) error { @@ -530,7 +545,7 @@ func (w *AtomicWrite) AddMovement(r *driver.MovementRecord) error { logger.Debug(query, args) _, err = w.txn.Exec(query, args...) - return dbError(err) + return ttxDBError(err) } func (w *AtomicWrite) AddValidationRecord(txID string, meta map[string][]byte) error { @@ -548,10 +563,10 @@ func (w *AtomicWrite) AddValidationRecord(txID string, meta map[string][]byte) e logger.Debug(query, txID, len(md), now) _, err = w.txn.Exec(query, txID, md, now) - return dbError(err) + return ttxDBError(err) } -func dbError(err error) error { +func ttxDBError(err error) error { if err == nil { return nil } diff --git a/token/services/interop/htlc/script.go b/token/services/interop/htlc/script.go index f81105b89..753d2981a 100644 --- a/token/services/interop/htlc/script.go +++ b/token/services/interop/htlc/script.go @@ -137,6 +137,14 @@ func (s *ScriptOwnership) IsMine(tms *token.ManagementService, tok *token3.Token return ids, len(ids) != 0 } +func (w *ScriptOwnership) OwnerType(raw []byte) (string, []byte, error) { + owner, err := identity.UnmarshalTypedIdentity(raw) + if err != nil { + return "", nil, err + } + return owner.Type, owner.Identity, nil +} + func senderWallet(w *token.OwnerWallet) string { return "htlc.sender" + w.ID() } diff --git a/token/services/tokens/manager.go b/token/services/tokens/manager.go index aa2110bb0..0e5424a7a 100644 --- a/token/services/tokens/manager.go +++ b/token/services/tokens/manager.go @@ -88,7 +88,7 @@ func (cm *Manager) newTokens(tmsID token.TMSID) (*Tokens, error) { return nil, errors.WithMessagef(err, "failed to get tokendb for [%s]", tmsID) } - storage, err := NewDBStorage(cm.notifier, db, tmsID) + storage, err := NewDBStorage(cm.notifier, cm.authorization, db, tmsID) if err != nil { return nil, errors.WithMessagef(err, "failed to get token store for [%s]", tmsID) } diff --git a/token/services/tokens/storage.go b/token/services/tokens/storage.go index 321e87cfd..70f7ea86b 100644 --- a/token/services/tokens/storage.go +++ b/token/services/tokens/storage.go @@ -30,10 +30,11 @@ type DBStorage struct { notifier events.Publisher tokenDB *tokendb.DB tmsID token.TMSID + ote OwnerTypeExtractor } -func NewDBStorage(notifier events.Publisher, tokenDB *tokendb.DB, tmsID token.TMSID) (*DBStorage, error) { - return &DBStorage{notifier: notifier, tokenDB: tokenDB, tmsID: tmsID}, nil +func NewDBStorage(notifier events.Publisher, ote OwnerTypeExtractor, tokenDB *tokendb.DB, tmsID token.TMSID) (*DBStorage, error) { + return &DBStorage{notifier: notifier, ote: ote, tokenDB: tokenDB, tmsID: tmsID}, nil } func (d *DBStorage) NewTransaction() (*transaction, error) { @@ -41,7 +42,12 @@ func (d *DBStorage) NewTransaction() (*transaction, error) { if err != nil { return nil, err } - return NewTransaction(d.notifier, tx, d.tmsID) + return &transaction{ + notifier: d.notifier, + tx: tx, + tmsID: d.tmsID, + ote: d.ote, + }, nil } func (d *DBStorage) StorePublicParams(raw []byte) error { @@ -52,6 +58,11 @@ type transaction struct { notifier events.Publisher tx *tokendb.Transaction tmsID token.TMSID + ote OwnerTypeExtractor +} + +type OwnerTypeExtractor interface { + OwnerType(raw []byte) (string, []byte, error) } func NewTransaction(notifier events.Publisher, tx *tokendb.Transaction, tmsID token.TMSID) (*transaction, error) { @@ -63,7 +74,7 @@ func NewTransaction(notifier events.Publisher, tx *tokendb.Transaction, tmsID to } func (t *transaction) DeleteToken(txID string, index uint64, deletedBy string) error { - tok, err := t.tx.GetToken(txID, index, true) + tok, owners, err := t.tx.GetToken(txID, index, true) if err != nil { return errors.WithMessagef(err, "failed to get token [%s:%d]", txID, index) } @@ -79,10 +90,6 @@ func (t *transaction) DeleteToken(txID string, index uint64, deletedBy string) e logger.Debugf("nothing further to delete for [%s:%d]", txID, index) return nil } - owners, err := t.tx.OwnersOf(txID, index) - if err != nil { - return errors.WithMessagef(err, "failed to get owners for token [%s:%d]", txID, index) - } for _, owner := range owners { logger.Debugf("post new delete-token event [%s:%s:%s]", txID, index, owner) t.Notify(DeleteToken, t.tmsID, owner, tok.Type, txID, index) @@ -114,12 +121,21 @@ func (t *transaction) AppendToken( if err != nil { return errors.Wrapf(err, "cannot covert [%s] with precision [%d]", tok.Quantity, precision) } + + typ, id, err := t.ote.OwnerType(tok.Owner.Raw) + if err != nil { + logger.Errorf("could not unmarshal identity when storing token: %s", err.Error()) + return errors.Wrap(err, "could not unmarshal identity when storing token") + } + err = t.tx.StoreToken( tokendb.TokenRecord{ TxID: txID, Index: index, IssuerRaw: issuer, OwnerRaw: tok.Owner.Raw, + OwnerType: typ, + OwnerIdentity: id, Ledger: tokenOnLedger, LedgerMetadata: tokenOnLedgerMetadata, Quantity: tok.Quantity, diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index 6ec00b551..83d8b4d78 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -25,9 +25,11 @@ var logger = flogging.MustGetLogger("token-sdk.tokens") type Authorization interface { // IsMine returns true if the passed token is owned by an owner wallet in the passed TMS IsMine(tms *token.ManagementService, tok *token2.Token) ([]string, bool) - // AmIAnAuditor return true if the passed TMS contains an auditor wallet for any of the auditor identities + // AmIAnAuditor returns true if the passed TMS contains an auditor wallet for any of the auditor identities // defined in the public parameters of the passed TMS. AmIAnAuditor(tms *token.ManagementService) bool + // OwnerType returns the type of owner (e.g. 'idemix' or 'htlc') and the identity bytes + OwnerType(raw []byte) (string, []byte, error) } type Issued interface { diff --git a/token/services/ttxdb/db.go b/token/services/ttxdb/db.go index dd05b4302..0008f8358 100644 --- a/token/services/ttxdb/db.go +++ b/token/services/ttxdb/db.go @@ -230,7 +230,7 @@ func (d *DB) AppendTransactionRecord(req *token.Request) error { if err != nil { return errors.WithMessagef(err, "begin update for txid [%s] failed", record.Anchor) } - if err := w.AddTokenRequest(record.Anchor, raw); err != nil { + if err := w.AddTokenRequest(record.Anchor, raw, req.Metadata.Application); err != nil { w.Rollback() return errors.WithMessagef(err, "append token request for txid [%s] failed", record.Anchor) } @@ -299,7 +299,8 @@ func (d *DB) AppendValidationRecord(txID string, tokenRequest []byte, meta map[s if err != nil { return errors.WithMessagef(err, "begin update for txid [%s] failed", txID) } - if err := w.AddTokenRequest(txID, tokenRequest); err != nil { + // we store the token request, but don't have or care about the application metadata + if err := w.AddTokenRequest(txID, tokenRequest, nil); err != nil { w.Rollback() return errors.WithMessagef(err, "append token request for txid [%s] failed", txID) }