Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4a430c1
Add sorting options to ListTokensOptions
May 2, 2026
9a3a1f0
Add IsSpent and SpentBy fields to IssuedToken
May 2, 2026
2473c57
Add IssuedBalance, RedeemedTokens, RedeemedBalance to IssuerWallet in…
May 2, 2026
b64b6c9
Add IssuedBalance, ListRedeemedTokens, RedeemedBalance to IssuerToken…
May 2, 2026
9a2c896
Implement IssuedBalance, ListRedeemedTokens, RedeemedBalance SQL queries
May 2, 2026
66a54c7
Implement IssuerWallet concrete methods for balance and redeemed tokens
May 2, 2026
96ce2ad
Expose IssuedBalance, ListRedeemedTokens, RedeemedBalance in public API
May 2, 2026
58bbb69
Regenerate mocks and add new methods to TokenVault interface
May 2, 2026
41b2f41
Fix nlreturn lint errors in IssuerWallet methods
May 2, 2026
36f42b2
Wire TokenType filter into IssuedBalance, ListRedeemedTokens, Redeeme…
May 2, 2026
369453e
Add issuer identity filter to IssuedBalance, ListRedeemedTokens, Rede…
May 2, 2026
e24072b
Add time-range filtering, ORDER BY, OutstandingBalance, and unit tests
May 2, 2026
882bf0d
Extend fungible integration test with issuer balance APIs
May 4, 2026
f4347ce
Exercise WithSortBy and WithTimeRange in tests; rename WithBalanceTyp…
May 5, 2026
b0354c9
fix: unmarshal CallView result as []byte before asserting uint64
May 8, 2026
0f35fb2
fix: use self-JOIN on tokens table for redeemed token queries
May 11, 2026
b668b26
Fix balance API mismatches after rebase
May 15, 2026
2ef5694
Fix remaining static-analysis and scan issues after balance API migra…
May 15, 2026
cb51370
Fix wallet and SQL lint regressions
May 15, 2026
59fb78a
CI: install configtxgen for all integration jobs
May 15, 2026
dca5cce
CI: enforce FAB_BINS and verify fabric binaries
May 15, 2026
0716287
CI: export FAB_BINS in integration run step
May 15, 2026
458d9b9
Fix nil balance panic in endorsement accept path
May 17, 2026
7a750b0
fix: correct IssuedBalance/RedeemedBalance queries and add TRedeemedB…
May 17, 2026
c3dfded
fix: correct IssuedBalance/RedeemedBalance queries and add TRedeemedB…
May 17, 2026
0dbe2a5
fix: correct IssuedBalance and RedeemedBalance to handle partial rede…
May 17, 2026
9d4a7b8
fix: propagate action-level issuer to burn outputs for RedeemedBalanc…
May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ jobs:
itest:
needs: checks
runs-on: ubuntu-latest
env:
FAB_BINS: ${{ github.workspace }}/../fabric/bin
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -150,6 +152,18 @@ jobs:
- name: Download fabric binaries
run: make download-fabric

- name: Verify fabric binaries
run: |
echo "FAB_BINS=${FAB_BINS}"
ls -la "${FAB_BINS}"
for bin in configtxgen configtxlator cryptogen discover orderer osnadmin peer; do
if [ ! -f "${FAB_BINS}/${bin}" ]; then
echo "Missing required binary: ${FAB_BINS}/${bin}"
exit 1
fi
chmod +x "${FAB_BINS}/${bin}"
done

- name: Docker
run: make docker-images

Expand All @@ -161,6 +175,9 @@ jobs:
- name: Run ${{ matrix.tests }}

run: |
export FAB_BINS="${FAB_BINS}"
echo "Running integration with FAB_BINS=${FAB_BINS}"
ls -la "${FAB_BINS}"
mkdir -p covdata
GOCOVERDIR=$(realpath covdata) make integration-tests-${{ matrix.tests }}
go tool covdata textfmt -i=covdata -o coverage.profile
Expand Down
2 changes: 1 addition & 1 deletion integration/token/dvp/views/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (b *BalanceView) Call(context view.Context) (interface{}, error) {
return nil, fmt.Errorf("wallet %s not found: %w", b.Wallet, err)
}

balance, err := wallet.Balance(context.Context(), token.WithType(b.Type))
balance, err := wallet.Balance(context.Context(), token.WithBalanceTokenType(b.Type))
if err != nil {
return nil, err
}
Expand Down
3 changes: 3 additions & 0 deletions integration/token/fungible/sdk/issuer/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ func (p *SDK) Install() error {
registry.RegisterFactory("balance", &views.BalanceViewFactory{}),
registry.RegisterFactory("historyIssuedToken", &views.ListIssuedTokensViewFactory{}),
registry.RegisterFactory("issuedTokenQuery", &views.ListIssuedTokensViewFactory{}),
registry.RegisterFactory("issuedBalance", &views.IssuedBalanceViewFactory{}),
registry.RegisterFactory("redeemedBalance", &views.RedeemedBalanceViewFactory{}),
registry.RegisterFactory("outstandingBalance", &views.OutstandingBalanceViewFactory{}),
registry.RegisterFactory("GetEnrollmentID", &views.GetEnrollmentIDViewFactory{}),
registry.RegisterFactory("acceptedTransactionHistory", &views.ListAcceptedTransactionsViewFactory{}),
registry.RegisterFactory("transactionInfo", &views.TransactionInfoViewFactory{}),
Expand Down
46 changes: 46 additions & 0 deletions integration/token/fungible/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,36 @@ func ListUnspentTokens(network *integration.Infrastructure, id *token3.NodeRefer
return ListUnspentTokensForTMSID(network, id, wallet, typ, nil)
}

func CheckIssuedBalance(network *integration.Infrastructure, issuer *token3.NodeReference, wallet string, typ token.Type, expected uint64) {
res, err := network.Client(issuer.ReplicaName()).CallView("issuedBalance", common.JSONMarshall(&views.IssuerBalanceQuery{
Wallet: wallet,
TokenType: typ,
}))
gomega.Expect(err).NotTo(gomega.HaveOccurred())
balance := JSONUnmarshalUint64(res)
gomega.Expect(balance).To(gomega.Equal(expected), "issued balance: got %d, expected %d", balance, expected)
}

func CheckRedeemedBalance(network *integration.Infrastructure, issuer *token3.NodeReference, wallet string, typ token.Type, expected uint64) {
res, err := network.Client(issuer.ReplicaName()).CallView("redeemedBalance", common.JSONMarshall(&views.IssuerBalanceQuery{
Wallet: wallet,
TokenType: typ,
}))
gomega.Expect(err).NotTo(gomega.HaveOccurred())
balance := JSONUnmarshalUint64(res)
gomega.Expect(balance).To(gomega.Equal(expected), "redeemed balance: got %d, expected %d", balance, expected)
}

func CheckOutstandingBalance(network *integration.Infrastructure, issuer *token3.NodeReference, wallet string, typ token.Type, expected uint64) {
res, err := network.Client(issuer.ReplicaName()).CallView("outstandingBalance", common.JSONMarshall(&views.IssuerBalanceQuery{
Wallet: wallet,
TokenType: typ,
}))
gomega.Expect(err).NotTo(gomega.HaveOccurred())
balance := JSONUnmarshalUint64(res)
gomega.Expect(balance).To(gomega.Equal(expected), "outstanding balance: got %d, expected %d", balance, expected)
}

func ListUnspentTokensForTMSID(network *integration.Infrastructure, id *token3.NodeReference, wallet string, typ token.Type, tmsId *token2.TMSID) *token.UnspentTokens {
res, err := network.Client(id.ReplicaName()).CallView("history", common.JSONMarshall(&views.ListUnspentTokens{
Wallet: wallet,
Expand Down Expand Up @@ -1224,6 +1254,22 @@ func JSONUnmarshalFloat64(v interface{}) float64 {
return s
}

func JSONUnmarshalUint64(v interface{}) uint64 {
var s uint64
switch v := v.(type) {
case []byte:
err := json.Unmarshal(v, &s)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
case string:
err := json.Unmarshal([]byte(v), &s)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
default:
panic(fmt.Sprintf("type not recognized [%T]", v))
}

return s
}

type deleteVaultPlatform interface {
DeleteVault(id string)
}
Expand Down
16 changes: 16 additions & 0 deletions integration/token/fungible/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ func TestAll(network *integration.Infrastructure, auditorId string, onRestart On
CheckAuditedTransactions(network, auditor, AuditedTransactions[:10], &t0, &t12)
CheckSpending(network, bob, "", "USD", auditor, 11)

// Verify issuer balance APIs after issue + redeem cycle.
// Default issuer wallet issued 110 + 10 (withdraw) + 10 (post-redeem) = 130 USD; redeemed 11 USD.
CheckIssuedBalance(network, issuer, "", "USD", 130)
CheckRedeemedBalance(network, issuer, "", "USD", 11)
CheckOutstandingBalance(network, issuer, "", "USD", 119)

// test multi action transfer...
t13 := time.Now()
IssueCash(network, "", "LIRA", 3, alice, auditor, true, issuer)
Expand Down Expand Up @@ -502,6 +508,16 @@ func TestAll(network *integration.Infrastructure, auditorId string, onRestart On
gomega.Expect(h.Sum(64).ToBigInt().Cmp(big.NewInt(180))).To(gomega.BeEquivalentTo(0))
gomega.Expect(h.ByType("EUR").Count()).To(gomega.BeEquivalentTo(h.Count()))

// Verify issuer balances after all issue and redeem operations.
// Default issuer: 241 USD issued, 21 redeemed (11 + 10).
CheckIssuedBalance(network, issuer, "", "USD", 241)
CheckRedeemedBalance(network, issuer, "", "USD", 21)
CheckOutstandingBalance(network, issuer, "", "USD", 220)
// New issuer wallet: 180 EUR issued, 0 redeemed.
CheckIssuedBalance(network, issuer, "newIssuerWallet", "EUR", 180)
CheckRedeemedBalance(network, issuer, "newIssuerWallet", "EUR", 0)
CheckOutstandingBalance(network, issuer, "newIssuerWallet", "EUR", 180)

CheckBalanceAndHolding(network, issuer, "", "USD", 110, auditor)
CheckBalanceAndHolding(network, issuer, "", "EUR", 150, auditor)
CheckBalanceAndHolding(network, issuer, "issuer.owner", "EUR", 10, auditor)
Expand Down
2 changes: 1 addition & 1 deletion integration/token/fungible/views/accept.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (a *AcceptCashView) Call(context view.Context) (interface{}, error) {
if output.Type == "MAX" {
continue
}
balance, err := ttx.MyWallet(context, token.WithTMSID(tx.TMSID())).Balance(context.Context(), ttx.WithType(output.Type))
balance, err := ttx.MyWallet(context, token.WithTMSID(tx.TMSID())).Balance(context.Context(), token.WithBalanceTokenType(output.Type))
assert.NoError(err, "failed retrieving balance for type [%s]", output.Type)
assert.True(balance.Cmp(big.NewInt(3000)) <= 0, "cannot have more than 3000 unspent quantity for type [%s]", output.Type)
}
Expand Down
2 changes: 1 addition & 1 deletion integration/token/fungible/views/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (b *BalanceView) Call(context view.Context) (interface{}, error) {
assert.NoError(err, "failed to compute the sum of the co-owned tokens")

if !b.SkipCheck {
balance, err := wallet.Balance(context.Context(), token.WithType(b.Type))
balance, err := wallet.Balance(context.Context(), token.WithBalanceTokenType(b.Type))
if err != nil {
return nil, err
}
Expand Down
82 changes: 82 additions & 0 deletions integration/token/fungible/views/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,85 @@ func (p *TransactionInfoViewFactory) NewView(in []byte) (view.View, error) {

return f, nil
}

// IssuerBalanceQuery contains the input parameters for issuer balance views.
type IssuerBalanceQuery struct {
Wallet string
TokenType token2.Type
TMSID *token.TMSID
}

// IssuedBalanceView returns the total amount of non-redeemed tokens issued by the given wallet.
type IssuedBalanceView struct {
*IssuerBalanceQuery
}

func (p *IssuedBalanceView) Call(context view.Context) (interface{}, error) {
wallet := ttx.GetIssuerWallet(context, p.Wallet, ServiceOpts(p.TMSID)...)
if wallet == nil {
return nil, errors.Errorf("wallet [%s] not found", p.Wallet)
}

return wallet.IssuedBalance(context.Context(), token.WithBalanceTokenType(p.TokenType))
}

type IssuedBalanceViewFactory struct{}

func (f *IssuedBalanceViewFactory) NewView(in []byte) (view.View, error) {
v := &IssuedBalanceView{IssuerBalanceQuery: &IssuerBalanceQuery{}}
if err := json.Unmarshal(in, v.IssuerBalanceQuery); err != nil {
return nil, errors.Wrapf(err, "failed unmarshalling input")
}

return v, nil
}

// RedeemedBalanceView returns the total amount of redeemed tokens originally issued by the given wallet.
type RedeemedBalanceView struct {
*IssuerBalanceQuery
}

func (p *RedeemedBalanceView) Call(context view.Context) (interface{}, error) {
wallet := ttx.GetIssuerWallet(context, p.Wallet, ServiceOpts(p.TMSID)...)
if wallet == nil {
return nil, errors.Errorf("wallet [%s] not found", p.Wallet)
}

return wallet.RedeemedBalance(context.Context(), token.WithBalanceTokenType(p.TokenType))
}

type RedeemedBalanceViewFactory struct{}

func (f *RedeemedBalanceViewFactory) NewView(in []byte) (view.View, error) {
v := &RedeemedBalanceView{IssuerBalanceQuery: &IssuerBalanceQuery{}}
if err := json.Unmarshal(in, v.IssuerBalanceQuery); err != nil {
return nil, errors.Wrapf(err, "failed unmarshalling input")
}

return v, nil
}

// OutstandingBalanceView returns IssuedBalance − RedeemedBalance for the given wallet.
type OutstandingBalanceView struct {
*IssuerBalanceQuery
}

func (p *OutstandingBalanceView) Call(context view.Context) (interface{}, error) {
wallet := ttx.GetIssuerWallet(context, p.Wallet, ServiceOpts(p.TMSID)...)
if wallet == nil {
return nil, errors.Errorf("wallet [%s] not found", p.Wallet)
}

return wallet.OutstandingBalance(context.Context(), token.WithBalanceTokenType(p.TokenType))
}

type OutstandingBalanceViewFactory struct{}

func (f *OutstandingBalanceViewFactory) NewView(in []byte) (view.View, error) {
v := &OutstandingBalanceView{IssuerBalanceQuery: &IssuerBalanceQuery{}}
if err := json.Unmarshal(in, v.IssuerBalanceQuery); err != nil {
return nil, errors.Wrapf(err, "failed unmarshalling input")
}

return v, nil
}
2 changes: 1 addition & 1 deletion integration/token/interop/views/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (b *BalanceView) Call(context view.Context) (interface{}, error) {
precision := tms.PublicParametersManager().PublicParameters().Precision()

// owned
balance, err := wallet.Balance(context.Context(), token.WithType(b.Type))
balance, err := wallet.Balance(context.Context(), token.WithBalanceTokenType(b.Type))
assert.NoError(err, "failed to get unspent tokens")

htlcWallet := htlc.Wallet(context, wallet)
Expand Down
11 changes: 11 additions & 0 deletions token/core/common/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ func (w *WalletBasedAuthorization) AmIAnAuditor() bool {
// Issued returns true if the passed issuer issued the passed token
func (w *WalletBasedAuthorization) Issued(ctx context.Context, issuer token.Identity, tok *token2.Token) bool {
_, err := w.WalletService.IssuerWallet(ctx, issuer)
if err == nil {
return true
}

// In some setups (notably HSM-backed identities), the issuer identity serialized in
// transfer metadata may not byte-match the locally registered wallet identity.
// Fall back to checking if this node has a default issuer wallet configured.
if issuer.IsNone() {
return false
}
_, err = w.WalletService.IssuerWallet(ctx, "")

return err == nil
}
Expand Down
21 changes: 21 additions & 0 deletions token/core/common/authorization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,27 @@ func TestWalletBasedAuthorization(t *testing.T) {

assert.False(t, auth.Issued(context.Background(), issuer, tok))
})

t.Run("Issued_True_DefaultIssuerFallback", func(t *testing.T) {
localWS := &mock.WalletService{}
localAuth := &WalletBasedAuthorization{Logger: logger, PublicParameters: pp, WalletService: localWS}
issuer := driver.Identity("issuer-id-hsm-variant")
tok := &token2.Token{}
localWS.IssuerWalletReturnsOnCall(0, nil, errors.New("not issuer by raw identity"))
localWS.IssuerWalletReturnsOnCall(1, &mock.IssuerWallet{}, nil)

assert.True(t, localAuth.Issued(context.Background(), issuer, tok))
})

t.Run("Issued_False_EmptyIssuer_NoFallback", func(t *testing.T) {
localWS := &mock.WalletService{}
localAuth := &WalletBasedAuthorization{Logger: logger, PublicParameters: pp, WalletService: localWS}
issuer := driver.Identity(nil)
tok := &token2.Token{}
localWS.IssuerWalletReturns(nil, errors.New("not issuer"))

assert.False(t, localAuth.Issued(context.Background(), issuer, tok))
})
}

type mockAuth struct {
Expand Down
Loading
Loading