diff --git a/token/services/selector/testutils/test_cases.go b/token/services/selector/testutils/test_cases.go index e47b6003b1..9566161336 100644 --- a/token/services/selector/testutils/test_cases.go +++ b/token/services/selector/testutils/test_cases.go @@ -9,6 +9,7 @@ package testutils import ( "context" "fmt" + "math/big" "sync" "sync/atomic" "testing" @@ -173,7 +174,7 @@ func (m *enhancedManager) UpdateTokens(deleted []*token.ID, added []token.Unspen LedgerMetadata: []byte{}, Quantity: t.Quantity, Type: t.Type, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, diff --git a/token/services/storage/db/dbtest/tokenlock.go b/token/services/storage/db/dbtest/tokenlock.go index 94c8ada5fd..b4b6cdfc95 100644 --- a/token/services/storage/db/dbtest/tokenlock.go +++ b/token/services/storage/db/dbtest/tokenlock.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0 package dbtest import ( + "math/big" "testing" "time" @@ -79,7 +80,7 @@ func TestFully(t *testing.T, tokenDB driver3.TokenStore, tokenLockDB driver3.Tok LedgerMetadata: []byte{}, // Empty metadata Quantity: "0x64", // 100 in hex Type: "USD", - Amount: 100, + Amount: big.NewInt(100), Owner: true, } err = tokenTx.StoreToken(ctx, tokenRecord, []string{"owner1"}) diff --git a/token/services/storage/db/dbtest/tokens.go b/token/services/storage/db/dbtest/tokens.go index 2a9ff2a87f..326359ef32 100644 --- a/token/services/storage/db/dbtest/tokens.go +++ b/token/services/storage/db/dbtest/tokens.go @@ -8,6 +8,7 @@ package dbtest import ( "fmt" + "math/big" "strings" "sync" "testing" @@ -29,15 +30,7 @@ type cfgProvider func(string) driver2.Driver func TokensTest(t *testing.T, cfgProvider cfgProvider) { t.Helper() for _, c := range tokensCases { - t.Run(c.Name, func(xt *testing.T) { - driver := cfgProvider(c.Name) - db, err := driver.NewToken("") - require.NoError(xt, err) - tokenDB, ok := db.(TestTokenDB) - assert.True(xt, ok) - defer utils.IgnoreError(tokenDB.Close) - c.Fn(t, db.(TestTokenDB)) - }) + TokensTestCase(t, cfgProvider, c.Name, c.Fn) } for _, c := range TokenNotifierCases { @@ -60,6 +53,22 @@ func TokensTest(t *testing.T, cfgProvider cfgProvider) { } } +// TokensTestCase runs a single token store test case against a store built by cfgProvider. +// Use it for cases that only some backends can satisfy and that are therefore not part of +// the shared tokensCases list. +func TokensTestCase(t *testing.T, cfgProvider cfgProvider, name string, fn func(*testing.T, TestTokenDB)) { + t.Helper() + t.Run(name, func(xt *testing.T) { + driver := cfgProvider(name) + db, err := driver.NewToken("") + require.NoError(xt, err) + tokenDB, ok := db.(TestTokenDB) + assert.True(xt, ok) + defer utils.IgnoreError(tokenDB.Close) + fn(t, tokenDB) + }) +} + var tokensCases = []struct { Name string Fn func(*testing.T, TestTokenDB) @@ -75,6 +84,7 @@ var tokensCases = []struct { {"PublicParams", TPublicParams}, {"Certification", TCertification}, {"QueryTokenDetails", TQueryTokenDetails}, + {"AmountValidation", TAmountValidation}, {"TTokenTypes", TTokenTypes}, {"ListUnspentTokensByWallets", TListUnspentTokensByWallets}, {"GetDeletedTokensPendingSKICleanup", TGetDeletedTokensPendingSKICleanup}, @@ -97,7 +107,7 @@ func TTokenTransaction(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: TST, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -162,7 +172,7 @@ func TSaveAndGetToken(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: TST, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -180,7 +190,7 @@ func TSaveAndGetToken(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: TST, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -198,7 +208,7 @@ func TSaveAndGetToken(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: TST, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -216,7 +226,7 @@ func TSaveAndGetToken(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: ABC, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -261,7 +271,7 @@ func TSaveAndGetToken(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: ABC, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -303,7 +313,7 @@ func TDeleteAndMine(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, @@ -320,7 +330,7 @@ func TDeleteAndMine(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, @@ -337,7 +347,7 @@ func TDeleteAndMine(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, @@ -384,7 +394,7 @@ func TListAuditTokens(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: false, Auditor: true, Issuer: false, @@ -401,7 +411,7 @@ func TListAuditTokens(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: false, Auditor: true, Issuer: false, @@ -418,7 +428,7 @@ func TListAuditTokens(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x03", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: false, Auditor: true, Issuer: false, @@ -459,7 +469,7 @@ func TListIssuedTokens(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: false, Auditor: false, Issuer: true, @@ -477,7 +487,7 @@ func TListIssuedTokens(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: false, Auditor: false, Issuer: true, @@ -495,7 +505,7 @@ func TListIssuedTokens(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x03", Type: "DEF", - Amount: 0, + Amount: big.NewInt(0), Owner: false, Auditor: false, Issuer: true, @@ -560,7 +570,7 @@ func TIssuerBalance(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: fmt.Sprintf("0x%02x", i.amount), Type: i.typ, - Amount: i.amount, + Amount: new(big.Int).SetUint64(i.amount), Owner: false, Auditor: false, Issuer: true, @@ -589,7 +599,7 @@ func TIssuerBalance(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: fmt.Sprintf("0x%02x", r.amount), Type: ABC, - Amount: r.amount, + Amount: new(big.Int).SetUint64(r.amount), Owner: false, Auditor: false, Issuer: true, @@ -641,7 +651,7 @@ func TGetTokenInfos(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte("tx101"), Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, @@ -658,7 +668,7 @@ func TGetTokenInfos(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte("tx102"), Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, @@ -675,7 +685,7 @@ func TGetTokenInfos(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte("tx102"), Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, @@ -743,6 +753,7 @@ func TDeleteMultiple(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, + Amount: big.NewInt(1), Owner: true, } require.NoError(t, db.StoreToken(t.Context(), tr, []string{"alice"})) @@ -756,6 +767,7 @@ func TDeleteMultiple(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, + Amount: big.NewInt(1), Owner: true, } require.NoError(t, db.StoreToken(t.Context(), tr, []string{"bob"})) @@ -769,6 +781,7 @@ func TDeleteMultiple(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, + Amount: big.NewInt(1), Owner: true, } require.NoError(t, db.StoreToken(t.Context(), tr, []string{"alice"})) @@ -845,6 +858,7 @@ func TCertification(t *testing.T, db TestTokenDB) { Ledger: []byte("ledger"), LedgerMetadata: []byte{}, Type: ABC, + Amount: big.NewInt(1), Owner: true, }, []string{"alice"}) if err != nil { @@ -899,6 +913,74 @@ func TCertification(t *testing.T, db TestTokenDB) { assert.Empty(t, certifications) } +// bigAmountRecord returns a token record whose amount is 2^64 + 1, one more than the +// largest value a uint64 can hold. +func bigAmountRecord() driver2.TokenRecord { + amount := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(1), 64), big.NewInt(1)) + + return driver2.TokenRecord{ + TxID: "txBig", + Index: 0, + IssuerRaw: []byte{}, + OwnerRaw: []byte{1, 2, 3}, + OwnerType: "idemix", + OwnerIdentity: []byte{}, + Ledger: []byte("ledger"), + LedgerMetadata: []byte{}, + Quantity: "0x" + amount.Text(16), + Type: TST, + Amount: amount, + Owner: true, + } +} + +// TAmountValidation verifies that an amount the amount column cannot hold is refused at +// write time instead of being stored as a value that disagrees with the quantity column. +func TAmountValidation(t *testing.T, db TestTokenDB) { + t.Helper() + ctx := t.Context() + + // 2^256 does not fit in NUMERIC(78, 0) and must be refused. + tooBig := bigAmountRecord() + tooBig.TxID = "txTooBig" + tooBig.Amount = new(big.Int).Lsh(big.NewInt(1), 256) + tooBig.Quantity = "0x" + tooBig.Amount.Text(16) + require.ErrorIs(t, db.StoreToken(ctx, tooBig, []string{"alice"}), driver2.ErrAmountOutOfRange) + + // A missing amount is refused as well: the column is NOT NULL. + noAmount := bigAmountRecord() + noAmount.TxID = "txNoAmount" + noAmount.Amount = nil + require.ErrorIs(t, db.StoreToken(ctx, noAmount, []string{"alice"}), driver2.ErrAmountMissing) + + // A record whose amount fits is accepted, and the authoritative quantity is preserved. + record := bigAmountRecord() + require.NoError(t, db.StoreToken(ctx, record, []string{"alice"})) + toks, err := db.ListUnspentTokens(ctx) + require.NoError(t, err) + require.Len(t, toks.Tokens, 1) + assert.Equal(t, record.Quantity, toks.Tokens[0].Quantity) +} + +// TBigAmountRoundTrip verifies that an amount that does not fit in a uint64 survives a +// write/read round-trip unchanged. It is not part of the shared tokensCases list because it +// requires a backend that keeps full precision in the amount column: SQLite gives a +// NUMERIC column NUMERIC affinity and silently converts an integer literal wider than +// int64 to REAL, so only Postgres can honour it. +func TBigAmountRoundTrip(t *testing.T, db TestTokenDB) { + t.Helper() + ctx := t.Context() + + record := bigAmountRecord() + require.NoError(t, db.StoreToken(ctx, record, []string{"alice"})) + + res, err := db.QueryTokenDetails(ctx, driver2.QueryTokenDetailsParams{WalletID: "alice"}) + require.NoError(t, err) + require.Len(t, res, 1) + assert.Zero(t, res[0].Amount.Cmp(record.Amount), + "amount [%s] was not stored as-is, got [%s]", record.Amount, res[0].Amount) +} + func TQueryTokenDetails(t *testing.T, db TestTokenDB) { t.Helper() ctx := t.Context() @@ -917,7 +999,7 @@ func TQueryTokenDetails(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: "TST1", - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -933,7 +1015,7 @@ func TQueryTokenDetails(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: TST, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -949,7 +1031,7 @@ func TQueryTokenDetails(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: TST, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -1057,7 +1139,7 @@ func TTokenTypes(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: TST, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -1074,7 +1156,7 @@ func TTokenTypes(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: "TST1", - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -1164,7 +1246,7 @@ func TListUnspentTokensByWallets(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: typ, - Amount: 2, + Amount: big.NewInt(2), Owner: true, }, owners)) } @@ -1180,7 +1262,7 @@ func TListUnspentTokensByWallets(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: typ, - Amount: 2, + Amount: big.NewInt(2), Owner: true, }, nil)) } @@ -1196,7 +1278,7 @@ func TListUnspentTokensByWallets(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x02", Type: typ, - Amount: 2, + Amount: big.NewInt(2), Owner: true, }, owners)) } @@ -1284,7 +1366,7 @@ func assertEqual(t *testing.T, r driver2.TokenRecord, d driver2.TokenDetails) { t.Helper() assert.Equal(t, r.TxID, d.TxID) assert.Equal(t, r.Index, d.Index) - assert.Equal(t, r.Amount, d.Amount.Uint64()) + assert.Zero(t, r.Amount.Cmp(d.Amount), "amount [%s] != [%s]", r.Amount, d.Amount) assert.Equal(t, r.OwnerType, d.OwnerType) } @@ -1304,7 +1386,7 @@ func TGetDeletedTokensPendingSKICleanup(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 1, + Amount: big.NewInt(1), Owner: true, } require.NoError(t, db.StoreToken(ctx, tr, []string{"alice"})) @@ -1330,7 +1412,7 @@ func TGetDeletedTokensPendingSKICleanup(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 1, + Amount: big.NewInt(1), Owner: true, } require.NoError(t, db.StoreToken(ctx, tr, []string{"alice"})) @@ -1564,7 +1646,7 @@ func TGetDeletedTokensPendingSKICleanup(t *testing.T, db TestTokenDB) { LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 1, + Amount: big.NewInt(1), Owner: false, Auditor: auditor, Issuer: issuer, diff --git a/token/services/storage/db/dbtest/tokensnotifier.go b/token/services/storage/db/dbtest/tokensnotifier.go index ba97f1115b..1aa6c87088 100644 --- a/token/services/storage/db/dbtest/tokensnotifier.go +++ b/token/services/storage/db/dbtest/tokensnotifier.go @@ -8,6 +8,7 @@ package dbtest import ( "context" + "math/big" "testing" "github.com/LFDT-Panurus/panurus/token/services/storage/db/driver" @@ -56,7 +57,7 @@ func TTokenNotifier(t *testing.T, db TestTokenDB, notifier driver.TokenNotifier) LedgerMetadata: []byte{}, Quantity: "0x02", Type: TST, - Amount: 2, + Amount: big.NewInt(2), Owner: true, Auditor: false, Issuer: false, @@ -84,7 +85,7 @@ var tokenRecords = []driver.TokenRecord{ LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, @@ -100,7 +101,7 @@ var tokenRecords = []driver.TokenRecord{ LedgerMetadata: []byte{}, Quantity: "0x01", Type: ABC, - Amount: 0, + Amount: big.NewInt(0), Owner: true, Auditor: false, Issuer: false, diff --git a/token/services/storage/db/driver/token.go b/token/services/storage/db/driver/token.go index 40058789df..a3980e9df4 100644 --- a/token/services/storage/db/driver/token.go +++ b/token/services/storage/db/driver/token.go @@ -46,8 +46,9 @@ type TokenRecord struct { Quantity string // Type is the type of token Type token.Type - // Amount is the Quantity converted to decimal - Amount uint64 + // Amount is the Quantity converted to decimal as a big integer to support arbitrary + // precision. It must be non-nil and match Quantity. + Amount *big.Int // Owner is used to mark the token as owned by this node Owner bool // Auditor is used to mark this token as audited by this node @@ -354,3 +355,10 @@ type TokenLockStore interface { } var ErrTokenDoesNotExist = errors.New("token does not exist") + +// ErrAmountMissing is returned when a record carries no amount. The amount column is NOT NULL. +var ErrAmountMissing = errors.New("no amount specified") + +// ErrAmountOutOfRange is returned when an amount is too wide for the amount column to hold. +// Storing it would leave the amount disagreeing with the authoritative quantity. +var ErrAmountOutOfRange = errors.New("amount exceeds maximum supported size") diff --git a/token/services/storage/db/sql/common/tokens.go b/token/services/storage/db/sql/common/tokens.go index 534e8f4da2..546e1deb5b 100644 --- a/token/services/storage/db/sql/common/tokens.go +++ b/token/services/storage/db/sql/common/tokens.go @@ -1636,6 +1636,11 @@ func (t *TokenTransaction) StoreToken(ctx context.Context, tr driver.TokenRecord if len(tr.OwnerWalletID) == 0 && len(owners) == 0 && tr.Owner { return errors.Errorf("no owners specified [%s]", string(debug.Stack())) } + // The amount column is NUMERIC(78, 0) NOT NULL: refuse a missing or over-range value + // rather than storing one that disagrees with the authoritative quantity column. + if err := validateAmount(tr.Amount); err != nil { + return errors.WithMessagef(err, "invalid amount for token [%s:%d]", tr.TxID, tr.Index) + } // Store token query, args := q.InsertInto(t.table.Tokens). @@ -1653,7 +1658,7 @@ func (t *TokenTransaction) StoreToken(ctx context.Context, tr driver.TokenRecord tr.LedgerMetadata, tr.Type, tr.Quantity, - tr.Amount, + tr.Amount.String(), time.Now().UTC(), tr.Owner, tr.Auditor, diff --git a/token/services/storage/db/sql/common/tokens_bench_test_util.go b/token/services/storage/db/sql/common/tokens_bench_test_util.go index 088105047a..a5d3a83f63 100644 --- a/token/services/storage/db/sql/common/tokens_bench_test_util.go +++ b/token/services/storage/db/sql/common/tokens_bench_test_util.go @@ -7,6 +7,7 @@ package common import ( "context" "fmt" + "math/big" "testing" "time" @@ -30,6 +31,7 @@ func SeedBenchTokens(b *testing.B, store *TokenStore, n int) { LedgerMetadata: []byte("meta"), Quantity: "0x64", Type: tokentype.Type("GOLD"), + Amount: big.NewInt(100), } if err := store.StoreToken(ctx, rec, []string{"wallet0"}); err != nil { b.Fatalf("seed failed at i=%d: %v", i, err) diff --git a/token/services/storage/db/sql/common/transactions.go b/token/services/storage/db/sql/common/transactions.go index 9031b3f011..88f494e5f0 100644 --- a/token/services/storage/db/sql/common/transactions.go +++ b/token/services/storage/db/sql/common/transactions.go @@ -12,6 +12,7 @@ import ( "encoding/json" errors2 "errors" "fmt" + "math/big" "strings" "time" @@ -37,6 +38,20 @@ import ( // 255 is used as a conservative safe upper bound. const maxAmountBits = 255 +// validateAmount reports whether amount can be stored in an amount column without loss. +// Callers must run it before inserting, so that a value the column cannot hold surfaces as +// an error instead of a row whose amount disagrees with its quantity. +func validateAmount(amount *big.Int) error { + if amount == nil { + return dbdriver.ErrAmountMissing + } + if amount.BitLen() > maxAmountBits { + return errors.WithMessagef(dbdriver.ErrAmountOutOfRange, "amount [%s] does not fit in %d bits", amount, maxAmountBits) + } + + return nil +} + type transactionTables struct { Movements string Transactions string @@ -637,8 +652,8 @@ func (w *TransactionStoreTransaction) AddTransaction(ctx context.Context, rs ... if err != nil { return errors.Wrapf(err, "error generating uuid") } - if r.Amount.BitLen() > maxAmountBits { - return errors.Errorf("amount [%s] exceeds maximum supported size of %d bits", r.Amount, maxAmountBits) + if err := validateAmount(r.Amount); err != nil { + return errors.WithMessagef(err, "invalid amount for record [%s]", r.TxID) } rows[i] = common3.Tuple{id, r.TxID, int(r.ActionType), r.SenderEID, r.RecipientEID, r.TokenType, r.Amount.String(), r.Timestamp.UTC()} } @@ -703,8 +718,8 @@ func (w *TransactionStoreTransaction) AddMovement(ctx context.Context, rs ...dbd if err != nil { return errors.Wrapf(err, "error generating uuid") } - if r.Amount.BitLen() > maxAmountBits { - return errors.Errorf("amount [%s] exceeds maximum supported size of %d bits", r.Amount, maxAmountBits) + if err := validateAmount(r.Amount); err != nil { + return errors.WithMessagef(err, "invalid amount for record [%s]", r.TxID) } rows[i] = common3.Tuple{id, r.TxID, r.EnrollmentID, r.TokenType, r.Amount.String(), now} } diff --git a/token/services/storage/db/sql/postgres/postgres_test.go b/token/services/storage/db/sql/postgres/postgres_test.go index d9bd7fb34f..7f112b5b86 100644 --- a/token/services/storage/db/sql/postgres/postgres_test.go +++ b/token/services/storage/db/sql/postgres/postgres_test.go @@ -22,7 +22,11 @@ func TestTokens(t *testing.T) { terminate, pgConnStr := startContainer(t) defer terminate() - dbtest2.TokensTest(t, func(name string) driver.Driver { return NewDriver(postgresCfg(pgConnStr, name)) }) + cfg := func(name string) driver.Driver { return NewDriver(postgresCfg(pgConnStr, name)) } + dbtest2.TokensTest(t, cfg) + // Only NUMERIC(78, 0) on Postgres keeps full precision for amounts wider than int64, + // so this case is not part of the shared suite. + dbtest2.TokensTestCase(t, cfg, "BigAmountRoundTrip", dbtest2.TBigAmountRoundTrip) } func TestTransactions(t *testing.T) { diff --git a/token/services/tokens/storage.go b/token/services/tokens/storage.go index 148b139cb2..c9322075c3 100644 --- a/token/services/tokens/storage.go +++ b/token/services/tokens/storage.go @@ -207,7 +207,7 @@ func (t *DBTransaction) AppendToken(ctx context.Context, tta TokenToAppend) erro LedgerMetadata: tta.TokenOnLedgerMetadata, Quantity: tta.Tok.Quantity, Type: tta.Tok.Type, - Amount: q.ToBigInt().Uint64(), + Amount: q.ToBigInt(), Owner: tta.Flags.Mine, Auditor: tta.Flags.Auditor, Issuer: tta.Flags.Issuer,