mirror of
https://github.com/luxfi/bank.git
synced 2026-10-30 17:02:07 +00:00
A market lookup comes back empty two ways and they are not the same fact. This chain carries no market for this asset is a property of the deployment, and Earn was always going to be a ledger loan. This chain cannot be reached is an outage. Both answered nil, so during an outage deposit and borrow returned 200 and the movement went onto the bank's own books: a borrow credited against collateral no chain is holding, sized from a position the chain overwrites the moment it returns, with real money paid out in between. Bank custody is only chosen when a chain is configured, so having nothing to ask can only mean an outage. It refuses now, the way Send already did and the way the wallet already declined to name an address — and the request is answered as the unreachable chain it is rather than as a missing identity. The test holds both halves: the refusal, and that no transaction row exists behind it, since a 502 with the money moved is the same bug wearing an error code. Co-authored-by: Hanzo Dev <[email protected]>
123 lines
4.2 KiB
Go
123 lines
4.2 KiB
Go
package bank
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/base/tests"
|
|
"github.com/luxfi/bank/collections"
|
|
)
|
|
|
|
// The vault catalog is public, like the plan ladder.
|
|
func TestVaultsPublic(t *testing.T) {
|
|
app := newBankApp(t)
|
|
run(t, app, tests.ApiScenario{
|
|
Name: "vault catalog is public",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/bank/vaults",
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{`"stlux"`, `"xLUX"`, `"maxLtv"`, `"wsteth"`},
|
|
})
|
|
}
|
|
|
|
// The seeded principal opens with two demo positions; borrowing past the LTV
|
|
// ceiling is refused, and a legitimate deposit + borrow settles.
|
|
func TestEarnFlow(t *testing.T) {
|
|
app := newBankApp(t)
|
|
_, token := seedPrincipal(t, app)
|
|
h := map[string]string{"Authorization": token, "Content-Type": "application/json"}
|
|
|
|
run(t, app, tests.ApiScenario{
|
|
Name: "earn/vaults folds the caller's positions into the catalog",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/bank/earn/vaults",
|
|
Headers: map[string]string{"Authorization": token},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{`"stlux"`, `"position"`, `"selfRepayDays"`},
|
|
})
|
|
|
|
run(t, app, tests.ApiScenario{
|
|
Name: "deposit adds collateral",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/bank/earn/deposit",
|
|
Body: strings.NewReader(`{"vault":"stlux","amount":20000000}`),
|
|
Headers: h,
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{`"collateralUsd"`, `"borrowable"`},
|
|
})
|
|
|
|
// Amounts are the vault asset's minor units on every verb, so this asks to
|
|
// borrow 200 LUX against 200 LUX of collateral — the whole of it, past the
|
|
// 90% the vault lends against.
|
|
run(t, app, tests.ApiScenario{
|
|
Name: "borrowing past the LTV ceiling is refused",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/bank/earn/borrow",
|
|
Body: strings.NewReader(`{"vault":"stlux","amount":200000000}`),
|
|
Headers: h,
|
|
ExpectedStatus: 422,
|
|
ExpectedContent: []string{"over the borrow limit"},
|
|
})
|
|
|
|
run(t, app, tests.ApiScenario{
|
|
Name: "a borrow within the limit settles",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/bank/earn/borrow",
|
|
Body: strings.NewReader(`{"vault":"stlux","amount":50000}`),
|
|
Headers: h,
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{`"debt"`, `"ltv"`},
|
|
})
|
|
}
|
|
|
|
// A configured chain that cannot be reached must stop an Earn movement, not
|
|
// quietly reroute it onto the ledger. The ledger path books a borrow against
|
|
// collateral the chain is not holding and sizes it from a position the chain
|
|
// overwrites the moment it returns — so the customer is paid real money against
|
|
// numbers that are about to be discarded.
|
|
//
|
|
// The refusal is only half the claim. The other half is that nothing moved: a
|
|
// 502 with a transaction row behind it would be the same bug wearing an error
|
|
// code.
|
|
func TestEarnRefusesWhileTheChainIsUnreachable(t *testing.T) {
|
|
t.Setenv("BANK_CHAIN_RPC", "http://127.0.0.1:1")
|
|
evmMu.Lock()
|
|
evmInst, evmFrom = nil, ""
|
|
evmMu.Unlock()
|
|
t.Cleanup(func() {
|
|
evmMu.Lock()
|
|
evmInst, evmFrom = nil, ""
|
|
evmMu.Unlock()
|
|
})
|
|
|
|
app := newBankApp(t)
|
|
_, token := seedPrincipal(t, app)
|
|
h := map[string]string{"Authorization": token, "Content-Type": "application/json"}
|
|
|
|
before, err := app.FindRecordsByFilter(collections.TransactionCollectionName, "type = 'earn'", "", 0, 0, nil)
|
|
if err != nil {
|
|
t.Fatalf("counting transactions: %v", err)
|
|
}
|
|
|
|
for _, act := range []string{"deposit", "borrow", "repay", "withdraw"} {
|
|
run(t, app, tests.ApiScenario{
|
|
Name: act + " refuses while the chain is unreachable",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/bank/earn/" + act,
|
|
Body: strings.NewReader(`{"vault":"stlux","amount":1000000}`),
|
|
Headers: h,
|
|
ExpectedStatus: http.StatusBadGateway,
|
|
ExpectedContent: []string{"the chain is unreachable"},
|
|
})
|
|
}
|
|
|
|
after, err := app.FindRecordsByFilter(collections.TransactionCollectionName, "type = 'earn'", "", 0, 0, nil)
|
|
if err != nil {
|
|
t.Fatalf("counting transactions: %v", err)
|
|
}
|
|
if len(after) != len(before) {
|
|
t.Errorf("%d earn transactions were written during an outage — the refusal did not stop the money", len(after)-len(before))
|
|
}
|
|
}
|