From 4180e0f7e7514701829ffd032c2aaa886b7adf11 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 May 2024 01:01:23 +0000 Subject: [PATCH 1/9] chore(deps): update dependency @algorandfoundation/algokit-client-generator to v3.0.3 --- pnpm-lock.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6253d3c5..a935de67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: devDependencies: '@algorandfoundation/algokit-client-generator': specifier: ^3.0.2 - version: 3.0.2 + version: 3.0.3 '@algorandfoundation/tealscript': specifier: '=0.92.0' version: 0.92.0 @@ -344,8 +344,8 @@ packages: resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} dev: true - /@algorandfoundation/algokit-client-generator@3.0.2: - resolution: {integrity: sha512-0/x2/szFPTXfJ21m3OlohfnMUM/qpz5vkPsFz06U2BQ8iHmtwFoIz94JLj9Z9mzYeTZBnVKvDgSqMNMkJRzldg==} + /@algorandfoundation/algokit-client-generator@3.0.3: + resolution: {integrity: sha512-yfTfLEm+pgXn7Ik87wzYZL/mgbDzdNdxyMR6A2neLCt9OivIN20km28L524yCGBdwutpJJFPIoeYS7mDgFVFZg==} engines: {node: '>=18.0'} hasBin: true dependencies: From e356a03d39e38c4a77bf3a90ca63d84a061b9c9a Mon Sep 17 00:00:00 2001 From: Patrick Bennett Date: Mon, 13 May 2024 01:00:56 -0400 Subject: [PATCH 2/9] fix(nodemgr): If node daemon needs to handle pool 'init' transaction, cover extra MBR for token opt-in if a reward token was specified for validator. --- nodemgr/internal/lib/reti/stakingpool.go | 16 ++++++++++------ nodemgr/internal/lib/reti/validator.go | 8 ++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/nodemgr/internal/lib/reti/stakingpool.go b/nodemgr/internal/lib/reti/stakingpool.go index 79f679e5..81482408 100644 --- a/nodemgr/internal/lib/reti/stakingpool.go +++ b/nodemgr/internal/lib/reti/stakingpool.go @@ -271,7 +271,7 @@ func (r *Reti) EpochBalanceUpdate(poolID int, poolAppID uint64, caller types.Add return nil } -func (r *Reti) GoOnline(poolAppID uint64, caller types.Address, offlineToOnline bool, votePK []byte, selectionPK []byte, stateProofPK []byte, voteFirst uint64, voteLast uint64, voteKeyDilution uint64) error { +func (r *Reti) GoOnline(poolAppID uint64, caller types.Address, needsIncentiveFeePaid bool, votePK []byte, selectionPK []byte, stateProofPK []byte, voteFirst uint64, voteLast uint64, voteKeyDilution uint64) error { var err error params, err := r.algoClient.SuggestedParams().Do(context.Background()) @@ -286,11 +286,11 @@ func (r *Reti) GoOnline(poolAppID uint64, caller types.Address, offlineToOnline params.Fee = transaction.MinTxnFee * 3 var goOnlineFee uint64 = 0 - if true { - // TODO - this is temporary - need to wait until AVM has opcode which can detect if acount is offline - // otherwise we always have to pay it - //if offlineToOnline { - // if going offline to online - pay extra 2 algo so the account is payouts eligible ! + // if going offline to online - pay extra 2 algo so the account is payouts eligible ! + if needsIncentiveFeePaid { + // TODO - this is temporary - need to wait until AVM has opcode which can detect if account isn't currently + // eligible for incentives and only then to pay the extra fee + // account return will also have IncentiveEligible property which caller will use when calling us. r.Logger.Info("paying extra fee for offline->online transition") goOnlineFee = 2e6 } @@ -382,5 +382,9 @@ func (r *Reti) PoolBalance(poolAppID uint64) uint64 { func (r *Reti) PoolAvailableRewards(poolAppID uint64, totalAlgoStaked uint64) uint64 { acctInfo, _ := algo.GetBareAccount(context.Background(), r.algoClient, crypto.GetApplicationAddress(poolAppID).String()) + if acctInfo.Amount < acctInfo.MinBalance { + // pool isn't properly initialized yet - so don't underflow on 'reward amount' + return 0 + } return acctInfo.Amount - totalAlgoStaked - acctInfo.MinBalance } diff --git a/nodemgr/internal/lib/reti/validator.go b/nodemgr/internal/lib/reti/validator.go index 3d6079bd..95cb0cae 100644 --- a/nodemgr/internal/lib/reti/validator.go +++ b/nodemgr/internal/lib/reti/validator.go @@ -996,14 +996,18 @@ func (r *Reti) CheckAndInitStakingPoolStorage(poolKey *ValidatorPoolKey) error { if err != nil { return err } + poolInitMbr := mbrs.PoolInitMbr + if r.Info().Config.RewardTokenId != 0 && poolKey.PoolId == 1 { + poolInitMbr += 100_000 // cover MBR of reward token asset + } // Now we have to pay MBR into the staking pool itself (!) and tell it to initialize itself gasMethod, _ := r.poolContract.GetMethodByName("gas") initStorageMethod, _ := r.poolContract.GetMethodByName("initStorage") - misc.Infof(r.Logger, "initializing staking pool storage, mbr payment to pool:%s", algo.FormattedAlgoAmount(mbrs.PoolInitMbr)) + misc.Infof(r.Logger, "initializing staking pool storage, mbr payment to pool:%s", algo.FormattedAlgoAmount(poolInitMbr)) atc := transaction.AtomicTransactionComposer{} - paymentTxn, err := transaction.MakePaymentTxn(managerAddr.String(), crypto.GetApplicationAddress(poolKey.PoolAppId).String(), mbrs.PoolInitMbr, nil, "", params) + paymentTxn, err := transaction.MakePaymentTxn(managerAddr.String(), crypto.GetApplicationAddress(poolKey.PoolAppId).String(), poolInitMbr, nil, "", params) payTxWithSigner := transaction.TransactionWithSigner{ Txn: paymentTxn, Signer: algo.SignWithAccountForATC(r.signer, managerAddr.String()), From 69a58bdf8084610e52e4060b18ab6fa7c39c96d8 Mon Sep 17 00:00:00 2001 From: Patrick Bennett Date: Mon, 13 May 2024 01:01:09 -0400 Subject: [PATCH 3/9] chore(nodemgr): Dependency updates --- nodemgr/go.mod | 17 ++++++----------- nodemgr/go.sum | 44 ++++++++++++-------------------------------- 2 files changed, 18 insertions(+), 43 deletions(-) diff --git a/nodemgr/go.mod b/nodemgr/go.mod index 1db6b3dc..b154b27c 100644 --- a/nodemgr/go.mod +++ b/nodemgr/go.mod @@ -3,18 +3,17 @@ module github.com/TxnLab/reti go 1.22.2 require ( - github.com/algorand/go-algorand-sdk/v2 v2.4.0 + github.com/algorand/go-algorand-sdk/v2 v2.5.0 github.com/antihax/optional v1.0.0 github.com/joho/godotenv v1.5.1 github.com/mailgun/holster/v4 v4.19.0 github.com/manifoldco/promptui v0.9.0 - github.com/prometheus/client_golang v1.19.0 + github.com/prometheus/client_golang v1.19.1 github.com/ssgreg/repeat v1.5.1 - github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v3 v3.0.0-alpha9 - golang.org/x/crypto v0.22.0 - golang.org/x/oauth2 v0.19.0 - golang.org/x/term v0.19.0 + golang.org/x/crypto v0.23.0 + golang.org/x/oauth2 v0.20.0 + golang.org/x/term v0.20.0 ) require ( @@ -23,15 +22,11 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.52.3 // indirect github.com/prometheus/procfs v0.13.0 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/sys v0.20.0 // indirect google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/nodemgr/go.sum b/nodemgr/go.sum index 6eb23c38..e861ebf1 100644 --- a/nodemgr/go.sum +++ b/nodemgr/go.sum @@ -2,8 +2,8 @@ github.com/ahmetb/go-linq v3.0.0+incompatible h1:qQkjjOXKrKOTy83X8OpRmnKflXKQIL/ github.com/ahmetb/go-linq v3.0.0+incompatible/go.mod h1:PFffvbdbtw+QTB0WKRP0cNht7vnCfnGlEpak/DVg5cY= github.com/algorand/avm-abi v0.2.0 h1:bkjsG+BOEcxUcnGSALLosmltE0JZdg+ZisXKx0UDX2k= github.com/algorand/avm-abi v0.2.0/go.mod h1:+CgwM46dithy850bpTeHh9MC99zpn2Snirb3QTl2O/g= -github.com/algorand/go-algorand-sdk/v2 v2.4.0 h1:R9ykarfk0ojAZlXlrysViDwWjHrvUMA0HmFHg9PmECw= -github.com/algorand/go-algorand-sdk/v2 v2.4.0/go.mod h1:Xk569fTpBTV0QtE74+79NTl6Rz3OC1K3iods4uG0ffU= +github.com/algorand/go-algorand-sdk/v2 v2.5.0 h1:7XgFrbH9V3Zz/t1916ruBiWrR4Oq1U4UsiwyQSlmt38= +github.com/algorand/go-algorand-sdk/v2 v2.5.0/go.mod h1:4ayerzjoWChm3kuVhbgFgURTbaYTtlj0c41eP3av5lw= github.com/algorand/go-codec/codec v1.1.10 h1:zmWYU1cp64jQVTOG8Tw8wa+k0VfwgXIPbnDfiVa+5QA= github.com/algorand/go-codec/codec v1.1.10/go.mod h1:YkEx5nmr/zuCeaDYOIhlDg92Lxju8tj2d2NrYqP7g7k= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= @@ -23,7 +23,6 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -34,14 +33,6 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailgun/holster/v4 v4.17.0 h1:+Zj3bunQFx3QkMWNlaeLolp7kyBteDnaPx7BT5hJidE= -github.com/mailgun/holster/v4 v4.17.0/go.mod h1:UeI3ynFAjOApzf/INq881b9WKM3eRNXp3oPFwJoJBMc= -github.com/mailgun/holster/v4 v4.18.0 h1:OwTN56t1eYiI6t+eTS3pPI3s0pA/JcV7cUIhLcsRbbk= -github.com/mailgun/holster/v4 v4.18.0/go.mod h1:UeI3ynFAjOApzf/INq881b9WKM3eRNXp3oPFwJoJBMc= github.com/mailgun/holster/v4 v4.19.0 h1:BQ390TMYg7CxBTMvZsHbq0fKnfRY7wxNb0uV+ZdwTJk= github.com/mailgun/holster/v4 v4.19.0/go.mod h1:/5ijRCyMjOHxt69WdAgvB2gyYCapJaJdT/QciGIcu50= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= @@ -50,20 +41,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= -github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= -github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= -github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/ssgreg/repeat v1.5.1 h1:8OjfXKWnFU9cL1cI+2UCdPpOpGOEax1oZ1FQdylri+8= @@ -74,21 +59,19 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo= github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -98,11 +81,11 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -116,8 +99,5 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 8daee42241218a59ae25ea4cf59e926750caf0d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 16:09:01 +0000 Subject: [PATCH 4/9] fix(ui): pin dependency @radix-ui/react-collapsible to 1.0.3 --- pnpm-lock.yaml | 2 +- ui/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a935de67..0dd6ffb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,7 +118,7 @@ importers: specifier: 1.0.4 version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-collapsible': - specifier: ^1.0.3 + specifier: 1.0.3 version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-dialog': specifier: 1.0.5 diff --git a/ui/package.json b/ui/package.json index 48e350bb..5591982e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,7 +45,7 @@ "@perawallet/connect": "1.3.4", "@radix-ui/react-avatar": "1.0.4", "@radix-ui/react-checkbox": "1.0.4", - "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-collapsible": "1.0.3", "@radix-ui/react-dialog": "1.0.5", "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-hover-card": "1.0.7", From 6b693b7869daacd87d0e5963bc93edae440846bd Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Mon, 13 May 2024 15:30:06 -0400 Subject: [PATCH 5/9] test(ui): add unit tests and JSDoc blocks for utility functions (#135) * test(ui): add tests/docs for bytes.ts * test(ui): add tests/docs for convert.ts * test(ui): add tests/docs for dayjs.ts * test(ui): add tests/docs for ellipseAddress.tsx * test(ui): add tests/docs for explorer.ts * test(ui): add tests/docs for format.ts * test(ui): add tests/docs for nfd.ts * test(ui): add tests/docs for paramsCache.ts * chore(ui): add JSDoc comments to utility functions * test(ui): add tests/docs and fixture data for contracts.ts This also refactors calculateMaxStake for simplicity and changes its return type to `bigint`, as I've been trying to keep all Algo/asset amounts as BigInts if possible. ValidatorTable is updated to convert the value to a number before passing it to AlgoAmount. (Hopefully a future version of algokit-utils will update this utility class to support BigInts.) * chore(ui): fix mock validator 2 id --- ui/src/components/ValidatorTable.tsx | 11 +- ui/src/utils/blocks.ts | 4 +- ui/src/utils/bytes.spec.ts | 61 ++++ ui/src/utils/bytes.ts | 36 +- ui/src/utils/contracts.spec.ts | 378 ++++++++++++++++---- ui/src/utils/contracts.ts | 233 +++++++++--- ui/src/utils/convert.spec.ts | 64 ++++ ui/src/utils/convert.ts | 41 ++- ui/src/utils/dayjs.spec.ts | 44 +++ ui/src/utils/dayjs.ts | 7 + ui/src/utils/ellipseAddress.spec.tsx | 22 +- ui/src/utils/ellipseAddress.tsx | 22 +- ui/src/utils/explorer.spec.ts | 42 +++ ui/src/utils/explorer.ts | 10 +- ui/src/utils/format.spec.ts | 90 ++++- ui/src/utils/format.ts | 124 ++++++- ui/src/utils/nfd.spec.ts | 104 ++++++ ui/src/utils/nfd.ts | 74 ++-- ui/src/utils/paramsCache.spec.ts | 68 ++++ ui/src/utils/paramsCache.ts | 22 +- ui/src/utils/tests/abi.ts | 54 +-- ui/src/utils/tests/fixtures/applications.ts | 4 +- ui/src/utils/tests/fixtures/boxes.ts | 7 +- ui/src/utils/tests/fixtures/methods.ts | 4 +- ui/src/utils/tests/fixtures/nfd.ts | 29 ++ ui/src/utils/tests/fixtures/validators.ts | 153 ++++++++ ui/src/utils/tests/utils.ts | 15 +- ui/src/utils/ui.ts | 15 +- ui/src/utils/validation.ts | 10 + 29 files changed, 1505 insertions(+), 243 deletions(-) create mode 100644 ui/src/utils/bytes.spec.ts create mode 100644 ui/src/utils/convert.spec.ts create mode 100644 ui/src/utils/dayjs.spec.ts create mode 100644 ui/src/utils/explorer.spec.ts create mode 100644 ui/src/utils/nfd.spec.ts create mode 100644 ui/src/utils/paramsCache.spec.ts create mode 100644 ui/src/utils/tests/fixtures/nfd.ts create mode 100644 ui/src/utils/tests/fixtures/validators.ts diff --git a/ui/src/components/ValidatorTable.tsx b/ui/src/components/ValidatorTable.tsx index 971d9db1..0225a530 100644 --- a/ui/src/components/ValidatorTable.tsx +++ b/ui/src/components/ValidatorTable.tsx @@ -177,15 +177,18 @@ export function ValidatorTable({ if (validator.state.numPools === 0) return '--' - const currentStake = AlgoAmount.MicroAlgos(Number(validator.state.totalAlgoStaked)).algos + const currentStakeAlgos = AlgoAmount.MicroAlgos( + Number(validator.state.totalAlgoStaked), + ).algos const currentStakeCompact = new Intl.NumberFormat(undefined, { notation: 'compact', - }).format(currentStake) + }).format(currentStakeAlgos) - const maxStake = calculateMaxStake(validator, constraints, true) + const maxStake = calculateMaxStake(validator, constraints) + const maxStakeAlgos = AlgoAmount.MicroAlgos(Number(maxStake)).algos const maxStakeCompact = new Intl.NumberFormat(undefined, { notation: 'compact', - }).format(maxStake) + }).format(maxStakeAlgos) return ( diff --git a/ui/src/utils/blocks.ts b/ui/src/utils/blocks.ts index 4d8dace1..378c9e40 100644 --- a/ui/src/utils/blocks.ts +++ b/ui/src/utils/blocks.ts @@ -1,9 +1,9 @@ import { dayjs } from '@/utils/dayjs' /** - * Calculate the average time between blocks + * Calculate the average time between Algorand blocks. * @param {number[]} timestamps - Block times, UNIX timestamps (in seconds) - * @returns {number} - Average time between blocks (in ms) + * @returns {number} Average time between blocks (in ms) */ export function calculateAverageBlockTime(timestamps: number[]): number { if (timestamps.length < 2) { diff --git a/ui/src/utils/bytes.spec.ts b/ui/src/utils/bytes.spec.ts new file mode 100644 index 00000000..e296bdde --- /dev/null +++ b/ui/src/utils/bytes.spec.ts @@ -0,0 +1,61 @@ +import algosdk from 'algosdk' +import { + concatUint8Arrays, + gatingValueFromBigint, + decodeUint8ArrayToBigint, + chunkBytes, +} from '@/utils/bytes' + +describe('concatUint8Arrays', () => { + it('should concatenate two Uint8Arrays', () => { + const a = new Uint8Array([1, 2, 3]) + const b = new Uint8Array([4, 5, 6]) + const result = concatUint8Arrays(a, b) + expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6])) + }) +}) + +describe('gatingValueFromBigint', () => { + it('should encode a bigint to a 32-byte long Uint8Array', () => { + const value = BigInt(123456789) + const result = gatingValueFromBigint(value) + const encodedValue = algosdk.encodeUint64(value) + const expected = new Uint8Array([...encodedValue, ...new Uint8Array(24)]) + expect(result).toEqual(expected) + }) +}) + +describe('decodeUint8ArrayToBigint', () => { + it('should decode a Uint8Array to a bigint', () => { + const value = BigInt(987654321) + const encodedValue = algosdk.encodeUint64(value) + const paddedValue = new Uint8Array([...encodedValue, ...new Uint8Array(24)]) + const result = decodeUint8ArrayToBigint(paddedValue) + expect(result).toBe(value) + }) + + it('should throw an error if the data is shorter than 8 bytes', () => { + const invalidData = new Uint8Array([1, 2, 3, 4, 5, 6, 7]) + expect(() => decodeUint8ArrayToBigint(invalidData)).toThrow( + 'Data is too short to contain a valid encoded bigint.', + ) + }) +}) + +describe('chunkBytes', () => { + it('should split a Uint8Array into chunks of a given size', () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + const result = chunkBytes(data, 4) + expect(result).toEqual([ + new Uint8Array([1, 2, 3, 4]), + new Uint8Array([5, 6, 7, 8]), + new Uint8Array([9, 10]), + ]) + }) + + it('should return an empty array when given an empty Uint8Array', () => { + const data = new Uint8Array([]) + const result = chunkBytes(data, 4) + expect(result).toEqual([]) + }) +}) diff --git a/ui/src/utils/bytes.ts b/ui/src/utils/bytes.ts index be612577..1584e967 100644 --- a/ui/src/utils/bytes.ts +++ b/ui/src/utils/bytes.ts @@ -1,5 +1,11 @@ import algosdk from 'algosdk' +/** + * Concatenates two Uint8Arrays into a single Uint8Array. + * @param {Uint8Array} a - The first Uint8Array + * @param {Uint8Array} b - The second Uint8Array + * @returns {Uint8Array} The concatenated Uint8Array + */ export function concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { const result = new Uint8Array(a.length + b.length) result.set(a) @@ -8,21 +14,29 @@ export function concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { } /** - * Encodes a bigint value into an 8-byte big-endian format and pads it to + * Encodes a BigInt value into an 8-byte big-endian format and pads it to * create a 32-byte long Uint8Array. * @param {bigint} value - The gating value to encode - * @returns {Uint8Array} - The encoded gating value + * @returns {Uint8Array} The encoded gating value + * @example + * gatingValueFromBigint(BigInt(123456789)) + * // Uint8Array [21, 205, 91, 7, 0, 0, 0, 0, 0, ...] */ export function gatingValueFromBigint(value: bigint): Uint8Array { return concatUint8Arrays(algosdk.encodeUint64(value), new Uint8Array(24)) } /** - * Decodes a Uint8Array back into a bigint. + * Decodes a Uint8Array back into a BigInt. * Assumes the Uint8Array was encoded in an 8-byte big-endian format, * followed by padding, similar to how `gatingValueFromBigint` encodes it. - * @param {Uint8Array} data - The Uint8Array to decode, expected to be 32 bytes long. - * @returns {bigint} - The decoded bigint value. + * @param {Uint8Array} data - The Uint8Array to decode, expected to be 32 bytes long + * @returns {bigint} The decoded bigint value + * @example + * const value = BigInt(987654321) + * const encodedValue = algosdk.encodeUint64(value) + * const paddedValue = new Uint8Array([...encodedValue, ...new Uint8Array(24)]) + * decodeUint8ArrayToBigint(paddedValue) // 987654321n */ export function decodeUint8ArrayToBigint(data: Uint8Array): bigint { if (data.length < 8) { @@ -42,8 +56,16 @@ export function decodeUint8ArrayToBigint(data: Uint8Array): bigint { /** * Splits a Uint8Array into chunks of a given size. * @param {Uint8Array} data - The Uint8Array to split into chunks - * @param {number} chunkSize - The size of each chunk - * @returns {Uint8Array[]} - An array of Uint8Array chunks + * @param {number} chunkSize - The size of each chunk (default: 64 [bytes]) + * @returns {Uint8Array[]} An array of Uint8Array chunks + * @example + * const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + * chunkBytes(data, 4) + * // [ + * // Uint8Array [1, 2, 3, 4], + * // Uint8Array [5, 6, 7, 8], + * // Uint8Array [9, 10], + * // ] */ export function chunkBytes(data: Uint8Array, chunkSize: number = 64): Uint8Array[] { const chunks: Uint8Array[] = [] diff --git a/ui/src/utils/contracts.spec.ts b/ui/src/utils/contracts.spec.ts index 606f1389..e76ca75a 100644 --- a/ui/src/utils/contracts.spec.ts +++ b/ui/src/utils/contracts.spec.ts @@ -1,72 +1,312 @@ -import { calculateRewardEligibility } from '@/utils/contracts' +import { + calculateMaxStake, + calculateRewardEligibility, + getEpochLengthBlocks, + isStakingDisabled, + isUnstakingDisabled, +} from '@/utils/contracts' +import { MOCK_CONSTRAINTS, MOCK_VALIDATOR_1, MOCK_VALIDATOR_2 } from './tests/fixtures/validators' +import { ACCOUNT_1 } from './tests/fixtures/accounts' + +describe('getEpochLengthBlocks', () => { + it('should return the input value directly for "blocks" timeframe', () => { + const inputValue = '100' + const epochTimeframe = 'blocks' + const expected = 100 + expect(getEpochLengthBlocks(inputValue, epochTimeframe)).toBe(expected) + }) + + it('should calculate correct number of blocks for "minutes"', () => { + const inputValue = '1' + const epochTimeframe = 'minutes' + const averageBlockTime = 60 * 1000 // 1 minute + const expected = 1 + expect(getEpochLengthBlocks(inputValue, epochTimeframe, averageBlockTime)).toBe(expected) + }) + + it('should calculate correct number of blocks for "hours"', () => { + const inputValue = '1' + const epochTimeframe = 'hours' + const averageBlockTime = 60 * 1000 // 1 minute + const expected = 60 + expect(getEpochLengthBlocks(inputValue, epochTimeframe, averageBlockTime)).toBe(expected) + }) + + it('should calculate correct number of blocks for "days"', () => { + const inputValue = '1' + const epochTimeframe = 'days' + const averageBlockTime = 60 * 1000 // 1 minute + const expected = 1440 + expect(getEpochLengthBlocks(inputValue, epochTimeframe, averageBlockTime)).toBe(expected) + }) + + it('should throw an error for negative average block time when timeframe is not "blocks"', () => { + const inputValue = '1' + const epochTimeframe = 'minutes' + const averageBlockTime = -100 // Negative block time + expect(() => getEpochLengthBlocks(inputValue, epochTimeframe, averageBlockTime)).toThrow( + 'Average block time must be greater than zero.', + ) + }) + + it('should throw an error for non-numeric value input', () => { + const inputValue = 'foo' + const epochTimeframe = 'hours' + const averageBlockTime = 60 * 1000 // 1 minute + expect(() => getEpochLengthBlocks(inputValue, epochTimeframe, averageBlockTime)).toThrow( + 'Value must be a number.', + ) + }) + + it('should return 0 for invalid timeframe', () => { + const inputValue = '10' + const epochTimeframe = 'foo' // Invalid timeframe + const averageBlockTime = 60 * 1000 // 1 minute + const expected = 0 + expect(getEpochLengthBlocks(inputValue, epochTimeframe, averageBlockTime)).toBe(expected) + }) +}) + +describe('calculateMaxStake', () => { + it('should return zero if the validator has no pools', () => { + const constraints = MOCK_CONSTRAINTS + const validator = { + ...MOCK_VALIDATOR_1, + state: { + ...MOCK_VALIDATOR_1.state, + numPools: 0, + totalStakers: 0, + totalAlgoStaked: 0n, + }, + } + expect(calculateMaxStake(validator, constraints)).toBe(BigInt(0)) + }) + + it('should return zero if constraints are not provided', () => { + const validator = MOCK_VALIDATOR_1 + expect(calculateMaxStake(validator)).toBe(BigInt(0)) + }) + + it('should calculate the correct maximum stake with default maxAlgoPerPool config', () => { + const constraints = MOCK_CONSTRAINTS + const validator1 = MOCK_VALIDATOR_1 // 1 pool, config.maxAlgoPerPool is 0n + const validator2 = MOCK_VALIDATOR_2 // 2 pools, config.maxAlgoPerPool is 0n + + const numPools1 = validator1.state.numPools // 2 pools + const defaultMaxStake1 = Number(constraints.maxAlgoPerPool) * numPools1 + const protocolMaxStake1 = Number(constraints.maxAlgoPerValidator) + const maxStake1 = Math.min(defaultMaxStake1, protocolMaxStake1) + expect(calculateMaxStake(validator1, constraints)).toBe(BigInt(maxStake1)) + + const numPools2 = validator2.state.numPools // 1 pool + const defaultMaxStake2 = Number(constraints.maxAlgoPerPool) * numPools2 + const protocolMaxStake2 = Number(constraints.maxAlgoPerValidator) + const maxStake2 = Math.min(defaultMaxStake2, protocolMaxStake2) + expect(calculateMaxStake(validator2, constraints)).toBe(BigInt(maxStake2)) + }) + + it('should calculate the correct maximum stake when custom maxAlgoPerPool is configured', () => { + const constraints = MOCK_CONSTRAINTS + const validator = { + ...MOCK_VALIDATOR_1, + config: { + ...MOCK_VALIDATOR_1.config, + maxAlgoPerPool: 10000000000000n, // 10M ALGO custom maxAlgoPerPool + }, + } + + const numPools = validator.state.numPools + const configuredMaxStake = Number(validator.config.maxAlgoPerPool) * numPools + const protocolMaxStake = Number(constraints.maxAlgoPerValidator) + const maxStake = Math.min(configuredMaxStake, protocolMaxStake) + expect(calculateMaxStake(validator, constraints)).toBe(BigInt(maxStake)) + }) + + it('should respect the protocol maximum when less than the calculated maximum', () => { + const constraints = { + ...MOCK_CONSTRAINTS, + maxAlgoPerValidator: BigInt(500000000000n), // 50M ALGO protocol max, lower than the default + } + const validator = MOCK_VALIDATOR_1 + + const numPools = validator.state.numPools + const defaultMaxStake = Number(constraints.maxAlgoPerPool) * numPools + const protocolMaxStake = Number(constraints.maxAlgoPerValidator) + const maxStake = Math.min(defaultMaxStake, protocolMaxStake) + expect(calculateMaxStake(validator, constraints)).toBe(BigInt(maxStake)) + }) +}) + +describe('isStakingDisabled', () => { + const activeAddress = ACCOUNT_1 + const constraints = MOCK_CONSTRAINTS + + it('should disable staking if no active address is provided', () => { + expect(isStakingDisabled(null, MOCK_VALIDATOR_1, constraints)).toBe(true) + }) + + it('should disable staking if the maximum number of stakers has been reached', () => { + const validator = { + ...MOCK_VALIDATOR_1, + state: { + ...MOCK_VALIDATOR_1.state, + totalStakers: 400, // Assuming this exceeds the maxStakersPerPool * numPools + }, + } + expect(isStakingDisabled(activeAddress, validator, constraints)).toBe(true) + }) + + it('should disable staking if the maximum stake has been reached', () => { + const validator = { + ...MOCK_VALIDATOR_1, + state: { + ...MOCK_VALIDATOR_1.state, + totalAlgoStaked: BigInt(constraints.maxAlgoPerValidator + 1000n), // Exceeds maxStake + }, + } + expect(isStakingDisabled(activeAddress, validator, constraints)).toBe(true) + }) + + it('should disable staking if no pools are available', () => { + const validator = { + ...MOCK_VALIDATOR_1, + state: { + ...MOCK_VALIDATOR_1.state, + numPools: 0, + }, + } + expect(isStakingDisabled(activeAddress, validator, constraints)).toBe(true) + }) + + it('should disable staking if the validator is sunsetted', () => { + const sunsettedValidator = { + ...MOCK_VALIDATOR_1, + config: { + ...MOCK_VALIDATOR_1.config, + sunsettingOn: Math.floor(Date.now() / 1000) - 10, // 10 seconds in the past + }, + } + expect(isStakingDisabled(activeAddress, sunsettedValidator, constraints)).toBe(true) + }) + + it('should allow staking under normal conditions', () => { + const normalValidator = { + ...MOCK_VALIDATOR_1, + config: { + ...MOCK_VALIDATOR_1.config, + sunsettingOn: 0, // Not sunsetted + }, + } + expect(isStakingDisabled(activeAddress, normalValidator, constraints)).toBe(false) + }) +}) + +describe('isUnstakingDisabled', () => { + const activeAddress = 'SOME_ACTIVE_ADDRESS' + + const validator = MOCK_VALIDATOR_1 + + const stakesWithValidator = [ + { + validatorId: 1, + balance: 1000n, + totalRewarded: 0n, + rewardTokenBalance: 0n, + entryTime: 1622548800, + lastPayout: 1625140800, + pools: [], + }, + ] + + const stakesWithoutValidator = [ + { + validatorId: 2, + balance: 1000n, + totalRewarded: 0n, + rewardTokenBalance: 0n, + entryTime: 1622548800, + lastPayout: 1625140800, + pools: [], + }, + ] + + it('should disable unstaking if no active address is provided', () => { + expect(isUnstakingDisabled(null, validator, stakesWithValidator)).toBe(true) + }) + + it('should disable unstaking if the validator has no pools', () => { + const validatorNoPools = { ...validator, state: { ...validator.state, numPools: 0 } } + expect(isUnstakingDisabled(activeAddress, validatorNoPools, stakesWithValidator)).toBe(true) + }) + + it('should disable unstaking if the validator has no associated stakes', () => { + expect(isUnstakingDisabled(activeAddress, validator, stakesWithoutValidator)).toBe(true) + }) + + it('should allow unstaking under normal conditions', () => { + expect(isUnstakingDisabled(activeAddress, validator, stakesWithValidator)).toBe(false) + }) +}) -// TODO needs changed for rounds instead of times describe('calculateRewardEligibility', () => { - // let currentTime: dayjs.Dayjs - - // beforeEach(() => { - // currentTime = dayjs() - // }) - - it('should return null if any of the input parameters are zero', () => { - expect(calculateRewardEligibility(0, 1625101200, 1625097600)).toBeNull() - expect(calculateRewardEligibility(30, 0, 1625097600)).toBeNull() - expect(calculateRewardEligibility(30, 1625101200, 0)).toBeNull() - }) - - it('should return null if any of the input parameters are undefined', () => { - expect(calculateRewardEligibility(undefined, 1625101200, 1625097600)).toBeNull() - expect(calculateRewardEligibility(30, undefined, 1625097600)).toBeNull() - expect(calculateRewardEligibility(30, 1625101200, undefined)).toBeNull() - }) - - // it('should calculate correct percentage when entry time and payout are in the past', () => { - // const epochLengthMins = 60 - // const lastPoolPayoutTime = currentTime.subtract(45, 'minutes').unix() // Last payout 45 minutes ago - // const entryTime = currentTime.subtract(15, 'minutes').unix() // Entry time 15 minutes ago - // expect(calculateRewardEligibility(epochLengthMins, lastPoolPayoutTime, entryTime)).toBe(50) - // }) - // - // it('should calculate correct percentage when entry time was in previous epoch', () => { - // const epochLengthMins = 30 - // const lastPoolPayoutTime = currentTime.subtract(20, 'minutes').unix() // Last payout 20 minutes ago - // const entryTime = currentTime.subtract(45, 'minutes').unix() // Entry time 45 minutes ago - // expect(calculateRewardEligibility(epochLengthMins, lastPoolPayoutTime, entryTime)).toBe(100) - // }) - // - // it('should handle edge case where next payout is exactly now', () => { - // const epochLengthMins = 60 - // const lastPoolPayoutTime = currentTime.subtract(1, 'hour').unix() // Last payout 1 hour ago - // const entryTime = currentTime.subtract(1, 'hour').unix() // Entry time 1 hour ago - // expect(calculateRewardEligibility(epochLengthMins, lastPoolPayoutTime, entryTime)).toBe(100) - // }) - // - // it('should handle postdated entry times correctly', () => { - // const epochLengthMins = 1 - // const lastPoolPayoutTime = currentTime.subtract(1, 'minutes').unix() // Last payout 1 minute ago - // const entryTime = currentTime.add(15, 'minutes').unix() // Entry time now (postdated 15 minutes) - // expect(calculateRewardEligibility(epochLengthMins, lastPoolPayoutTime, entryTime)).toBe(0) - // }) - // - // it('should round down to the nearest integer', () => { - // const epochLengthMins = 60 * 4 // 4 hours - // const lastPoolPayoutTime = currentTime.subtract(3, 'hours').unix() // Last payout 3 hours ago - // // Entry time 1 hour and 1 minute ago, exact eligibility is 50.416666666666664 - // const entryTime = currentTime.subtract(1, 'hour').subtract(1, 'minute').unix() - // expect(calculateRewardEligibility(epochLengthMins, lastPoolPayoutTime, entryTime)).toBe(50) - // }) - // - // it('should never return more than 100%', () => { - // const epochLengthMins = 60 - // const lastPoolPayoutTime = currentTime.subtract(3, 'hours').unix() // Last payout 3 hours ago - // const entryTime = currentTime.subtract(2, 'hours').unix() // Entry time 2 hours ago - // expect(calculateRewardEligibility(epochLengthMins, lastPoolPayoutTime, entryTime)).toBe(100) - // }) - // - // it('should never return less than 0%', () => { - // const epochLengthMins = 60 - // const lastPoolPayoutTime = currentTime.unix() // Current time - // const entryTime = currentTime.add(1, 'hour').unix() // Entry time in the future - // expect(calculateRewardEligibility(epochLengthMins, lastPoolPayoutTime, entryTime)).toBe(0) - // }) + it('should return null if any input parameter is zero', () => { + expect(calculateRewardEligibility(0, 1000, 500)).toBeNull() + expect(calculateRewardEligibility(10, 0, 500)).toBeNull() + expect(calculateRewardEligibility(10, 1000, 0)).toBeNull() + }) + + it('should return null if any input parameter is undefined', () => { + expect(calculateRewardEligibility(undefined, 1000, 500)).toBeNull() + expect(calculateRewardEligibility(10, undefined, 500)).toBeNull() + expect(calculateRewardEligibility(10, 1000, undefined)).toBeNull() + }) + + it('should calculate correct percentage when entry round and payout are in the past', () => { + const epochRoundLength = 100 + const lastPoolPayoutRound = 900 + const entryRound = 850 + expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(50) + }) + + it('should calculate correct percentage when entry round was in the previous epoch', () => { + const epochRoundLength = 50 + const lastPoolPayoutRound = 200 + const entryRound = 100 + expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(100) + }) + + it('should handle edge case where next payout is exactly now', () => { + const epochRoundLength = 60 + const lastPoolPayoutRound = 120 + const entryRound = 60 + expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(100) + }) + + it('should handle postdated entry rounds correctly', () => { + const epochRoundLength = 1 + const lastPoolPayoutRound = 2 + const entryRound = 20 // Postdated entry round + expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(0) + }) + + it('should round down to the nearest integer', () => { + const epochRoundLength = 100 + const lastPoolPayoutRound = 300 + const entryRound = 251 // Exact eligibility is 49% + expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(49) + }) + + it('should never return more than 100%', () => { + const epochRoundLength = 100 + const lastPoolPayoutRound = 300 + const entryRound = 200 + expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(100) + }) + + it('should never return less than 0%', () => { + const epochRoundLength = 100 + const lastPoolPayoutRound = 100 + const entryRound = 200 // Future round beyond the current epoch + expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(0) + }) }) diff --git a/ui/src/utils/contracts.ts b/ui/src/utils/contracts.ts index 7ca204cd..b9f7aea6 100644 --- a/ui/src/utils/contracts.ts +++ b/ui/src/utils/contracts.ts @@ -1,4 +1,3 @@ -import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' import { QueryClient } from '@tanstack/react-query' import algosdk from 'algosdk' import { fetchAccountInformation } from '@/api/algod' @@ -25,6 +24,11 @@ import { } from '@/interfaces/validator' import { dayjs } from '@/utils/dayjs' +/** + * Transform raw validator configuration data (from `callGetValidatorConfig`) into a structured object + * @param {RawValidatorConfig} rawConfig - Raw validator configuration data + * @returns {ValidatorConfig} Structured validator configuration object + */ export function transformValidatorConfig(rawConfig: RawValidatorConfig): ValidatorConfig { return { id: Number(rawConfig[0]), @@ -48,6 +52,11 @@ export function transformValidatorConfig(rawConfig: RawValidatorConfig): Validat } } +/** + * Transform raw validator state data (from `callGetValidatorState`) into a structured object + * @param {RawValidatorState} rawState - Raw validator state data + * @returns {ValidatorState} Structured validator state object + */ export function transformValidatorState(rawState: RawValidatorState): ValidatorState { return { numPools: Number(rawState[0]), @@ -57,6 +66,11 @@ export function transformValidatorState(rawState: RawValidatorState): ValidatorS } } +/** + * Transform raw staking pool data (from `callGetPools`) into structured objects + * @param {RawPoolsInfo} rawPoolsInfo - Raw staking pool data + * @returns {PoolInfo[]} Structured pool info objects + */ export function transformPoolsInfo(rawPoolsInfo: RawPoolsInfo): PoolInfo[] { return rawPoolsInfo.map((poolInfo) => ({ poolAppId: Number(poolInfo[0]), @@ -65,12 +79,22 @@ export function transformPoolsInfo(rawPoolsInfo: RawPoolsInfo): PoolInfo[] { })) } +/** + * Transform raw node pool assignment configuration data (from `callGetNodePoolAssignments`) into a flat array + * @param {RawNodePoolAssignmentConfig} rawConfig - Raw node pool assignment configuration data + * @returns {NodePoolAssignmentConfig} Flattened array of `NodeConfig` objects + */ export function transformNodePoolAssignment( rawConfig: RawNodePoolAssignmentConfig, ): NodePoolAssignmentConfig { return rawConfig[0].flat() } +/** + * Transform raw pool token payout ratio data (from `callGetTokenPayoutRatio`) into a flat array of payout ratios + * @param {RawPoolTokenPayoutRatios} rawData - Raw pool token payout ratio data + * @returns {number[]} Array of pool token payout ratios + */ export function transformPoolTokenPayoutRatio(rawData: RawPoolTokenPayoutRatios): number[] { const [poolPctOfWhole, updatedForPayout] = rawData @@ -82,6 +106,15 @@ export function transformPoolTokenPayoutRatio(rawData: RawPoolTokenPayoutRatios) return poolTokenPayoutRatio.poolPctOfWhole } +/** + * Transform raw validator data from multiple ABI method calls into a structured `Validator` object + * @param {RawValidatorConfig} rawConfig - Raw validator configuration data + * @param {RawValidatorState} rawState - Raw validator state data + * @param {RawPoolsInfo} rawPoolsInfo - Raw staking pool data + * @param {RawPoolTokenPayoutRatios} rawPoolTokenPayoutRatios - Raw pool token payout ratio data + * @param {RawNodePoolAssignmentConfig} rawNodePoolAssignment - Raw node pool assignment configuration data + * @returns {Validator} Structured validator object + */ export function transformValidatorData( rawConfig: RawValidatorConfig, rawState: RawValidatorState, @@ -105,6 +138,11 @@ export function transformValidatorData( } } +/** + * Transform raw staked info byte data from box storage into a structured `StakedInfo` object + * @param {Uint8Array} data - Raw staked info data (in a 64-byte chunk) + * @returns {StakedInfo} Structured staked info object + */ export function transformStakedInfo(data: Uint8Array): StakedInfo { return { account: algosdk.encodeAddress(data.slice(0, 32)), @@ -115,6 +153,12 @@ export function transformStakedInfo(data: Uint8Array): StakedInfo { } } +/** + * Process node pool assignment configuration data into an array with each node's available slot count + * @param {NodePoolAssignmentConfig} nodes - Node pool assignment configuration data + * @param {number} poolsPerNode - Number of pools per node + * @returns {NodeInfo[]} Array of objects containing node `index` and `availableSlots` + */ export function processNodePoolAssignment( nodes: NodePoolAssignmentConfig, poolsPerNode: number, @@ -131,6 +175,12 @@ export function processNodePoolAssignment( }) } +/** + * Check if a validator has available slots for more pools + * @param {NodePoolAssignmentConfig} nodePoolAssignmentConfig - Ordered array of single `NodeConfig` arrays per pool + * @param {number} poolsPerNode - Number of pools per node + * @returns {boolean} Whether the validator has available slots + */ export function validatorHasAvailableSlots( nodePoolAssignmentConfig: NodePoolAssignmentConfig, poolsPerNode: number, @@ -141,6 +191,12 @@ export function validatorHasAvailableSlots( }) } +/** + * Find the first available node with a slot for a new pool + * @param {NodePoolAssignmentConfig} nodePoolAssignmentConfig - Node pool assignment configuration data + * @param {number} poolsPerNode - Number of pools per node + * @returns {number | null} Node index with available slot, or null if no available slots found + */ export function findFirstAvailableNode( nodePoolAssignmentConfig: NodePoolAssignmentConfig, poolsPerNode: number, @@ -154,6 +210,13 @@ export function findFirstAvailableNode( return null // No available slot found } +/** + * Returns the number of blocks in a given timeframe based on the average block time + * @param {string} value - User provided value for epoch length + * @param {string} epochTimeframe - Selected epoch timeframe unit ('blocks', 'minutes', 'hours', 'days') + * @param {number} averageBlockTime - Average block time in milliseconds + * @returns {number} Number of blocks in the given timeframe + */ export function getEpochLengthBlocks( value: string, epochTimeframe: string, @@ -182,6 +245,14 @@ export function getEpochLengthBlocks( } } +/** + * Transform entry gating assets into a fixed length array of asset IDs + * @param {string} type - Entry gating type + * @param {Array<{ value: string }>} assets - Entry gating assets + * @param {number} nfdCreatorAppId - NFD creator app ID + * @param {number} nfdParentAppId - NFD parent app ID + * @returns {string[]} Fixed length array of asset IDs + */ export function transformEntryGatingAssets( type: string, assets: Array<{ value: string }>, @@ -205,33 +276,32 @@ export function transformEntryGatingAssets( } } -export function calculateMaxStake( - validator: Validator, - constraints?: Constraints, - algos = false, -): number { - const { numPools } = validator.state - if (numPools === 0 || !constraints) { - return 0 - } - const hardMaxDividedBetweenPools = constraints.maxAlgoPerValidator / BigInt(numPools) - let { maxAlgoPerPool } = validator.config - if (maxAlgoPerPool === 0n) { - maxAlgoPerPool = constraints.maxAlgoPerPool - } - if (hardMaxDividedBetweenPools < maxAlgoPerPool) { - maxAlgoPerPool = hardMaxDividedBetweenPools +/** + * Calculate the maximum total stake based on the validator's configuration and protocol constraints + * @param {Validator} validator - Validator object + * @param {Constraints} constraints - Protocol constraints object + * @returns {bigint} Maximum total stake + */ +export function calculateMaxStake(validator: Validator, constraints?: Constraints): bigint { + if (validator.state.numPools === 0 || !constraints) { + return BigInt(0) } - const maxStake = Number(maxAlgoPerPool) * numPools + const protocolMaxStake = constraints.maxAlgoPerValidator - if (algos) { - return AlgoAmount.MicroAlgos(maxStake).algos - } + const numPools = BigInt(validator.state.numPools) + const maxAlgoPerPool = validator.config.maxAlgoPerPool || constraints.maxAlgoPerPool + const maxStake = maxAlgoPerPool * numPools - return maxStake + return maxStake < protocolMaxStake ? maxStake : protocolMaxStake } +/** + * Calculate the maximum number of stakers based on the validator's configuration and protocol constraints + * @param {Validator} validator - Validator object + * @param {Constraints} constraints - Protocol constraints object + * @returns {number} Maximum number of stakers + */ export function calculateMaxStakers(validator: Validator, constraints?: Constraints): number { const maxStakersPerPool = constraints?.maxStakersPerPool || 0 const maxStakers = maxStakersPerPool * validator.state.numPools @@ -239,6 +309,13 @@ export function calculateMaxStakers(validator: Validator, constraints?: Constrai return maxStakers } +/** + * Check if staking is disabled based on the validator's state and protocol constraints + * @param {string | null} activeAddress - Active wallet address + * @param {Validator} validator - Validator object + * @param {Constraints} constraints - Protocol constraints object + * @returns {boolean} Whether staking is disabled + */ export function isStakingDisabled( activeAddress: string | null, validator: Validator, @@ -249,24 +326,25 @@ export function isStakingDisabled( } const { numPools, totalStakers, totalAlgoStaked } = validator.state - let { maxAlgoPerPool } = validator.config + const noPools = numPools === 0 - if (maxAlgoPerPool === 0n && !!constraints) { - maxAlgoPerPool = constraints.maxAlgoPerPool - } + const maxStake = calculateMaxStake(validator, constraints) + const maxStakeReached = Number(totalAlgoStaked) >= Number(maxStake) const maxStakersPerPool = constraints?.maxStakersPerPool || 0 - const maxStakers = maxStakersPerPool * numPools - const maxStake = Number(maxAlgoPerPool) * numPools - - const noPools = numPools === 0 const maxStakersReached = totalStakers >= maxStakers - const maxStakeReached = Number(totalAlgoStaked) >= maxStake return noPools || maxStakersReached || maxStakeReached || isSunsetted(validator) } +/** + * Check if unstaking is disabled based on the validator's state and staking data + * @param {string | null} activeAddress - Active wallet address + * @param {Validator} validator - Validator object + * @param {StakerValidatorData[]} stakesByValidator - Staking data for the active address + * @returns {boolean} Whether unstaking is disabled + */ export function isUnstakingDisabled( activeAddress: string | null, validator: Validator, @@ -281,6 +359,13 @@ export function isUnstakingDisabled( return noPools || !validatorHasStake } +/** + * Check if adding a pool is disabled based on the validator's state and protocol constraints + * @param {string | null} activeAddress - Active wallet address + * @param {Validator} validator - Validator object + * @param {Constraints} constraints - Protocol constraints object + * @returns {boolean} Whether adding a pool is disabled + */ export function isAddingPoolDisabled( activeAddress: string | null, validator: Validator, @@ -298,20 +383,41 @@ export function isAddingPoolDisabled( return !hasAvailableSlots || isSunsetted(validator) } +/** + * Check if a validator is sunsetting or has sunsetted + * @param {Validator} validator - Validator object + * @returns {boolean} Whether the validator is sunsetting or has sunsetted + */ export function isSunsetting(validator: Validator): boolean { return validator.config.sunsettingOn > 0 } +/** + * Check if a validator has sunsetted + * @param {Validator} validator - Validator object + * @returns {boolean} Whether the validator has sunsetted + */ export function isSunsetted(validator: Validator): boolean { return validator.config.sunsettingOn > 0 ? dayjs.unix(validator.config.sunsettingOn).isBefore(dayjs()) : false } +/** + * Check if a validator has a migration set + * @param {Validator} validator - Validator object + * @returns {boolean} Whether the validator has a migration set + */ export function isMigrationSet(validator: Validator): boolean { return validator.config.sunsettingTo > 0 } +/** + * Check if the active address can manage a validator + * @param {string | null} activeAddress - Active wallet address + * @param {Validator} validator - Validator object + * @returns {boolean} Whether the active address can manage the provided validator + */ export function canManageValidator(activeAddress: string | null, validator: Validator): boolean { if (!activeAddress) { return false @@ -320,6 +426,14 @@ export function canManageValidator(activeAddress: string | null, validator: Vali return owner === activeAddress || manager === activeAddress } +/** + * Returns the entry gating value to verify when adding stake. + * Depending on the gating type, network requests may be required to fetch additional data. + * @param {Validator | null} validator - Validator object + * @param {string | null} activeAddress - Active wallet address + * @param {AssetHolding[]} heldAssets - Assets held by the active address + * @returns {number} Entry gating value to verify, or 0 if none found + */ export async function fetchValueToVerify( validator: Validator | null, activeAddress: string | null, @@ -411,6 +525,13 @@ export async function fetchValueToVerify( return 0 } +/** + * Find the first gating asset held by the active address that meets the minimum balance requirement + * @param {AssetHolding[]} heldAssets - Assets held by the active address + * @param {number[]} gatingAssets - Array of gating assets + * @param {number} minBalance - Minimum balance required for gating assets + * @returns {number} Gating asset ID that meets the minimum balance requirement or 0 if not found + */ export function findValueToVerify( heldAssets: AssetHolding[], gatingAssets: number[], @@ -422,7 +543,16 @@ export function findValueToVerify( return asset?.['asset-id'] || 0 } -export function calculateMaxAvailableToStake(validator: Validator, constraints?: Constraints) { +/** + * Calculate the maximum amount of algo that can be staked based on the validator's configuration + * @param {Validator} validator - Validator object + * @param {Constraints} constraints - Protocol constraints object + * @returns {number} Maximum amount of algo that can be staked + */ +export function calculateMaxAvailableToStake( + validator: Validator, + constraints?: Constraints, +): number { let { maxAlgoPerPool } = validator.config if (maxAlgoPerPool === 0n) { @@ -442,11 +572,10 @@ export function calculateMaxAvailableToStake(validator: Validator, constraints?: } /** - * Calculate rewards eligibility percentage for a staker based on their entry time and last pool payout time - * - * @param {number} epochRoundLength Validator payout frequency in rounds - * @param {number} lastPoolPayoutRound Last pool payout round number - * @param {number} entryRound Staker entry time in Unix timestamp (15 min postdated) + * Calculate rewards eligibility percentage for a staker based on their entry round and last pool payout round. + * @param {number} epochRoundLength - Validator payout frequency in rounds + * @param {number} lastPoolPayoutRound - Last pool payout round number + * @param {number} entryRound - Staker entry round * @returns {number | null} Rewards eligibility percentage, or null if any input parameters are zero/undefined */ export function calculateRewardEligibility( @@ -454,28 +583,30 @@ export function calculateRewardEligibility( lastPoolPayoutRound: number = 0, entryRound: number = 0, ): number | null { - if (epochRoundLength == 0 || entryRound == 0 || lastPoolPayoutRound == 0) { + if (epochRoundLength === 0 || lastPoolPayoutRound === 0 || entryRound === 0) { return null } - // Calc next payout round - const nextEpoch = - lastPoolPayoutRound - (lastPoolPayoutRound % epochRoundLength) + epochRoundLength + // Calculate the next payout round + const currentEpochStartRound = lastPoolPayoutRound - (lastPoolPayoutRound % epochRoundLength) + const nextPayoutRound = currentEpochStartRound + epochRoundLength - // If the next payout time is in the past (i.e., no rewards last payout), there can't be a reward - if (nextEpoch < entryRound) { + // If the entry round is greater than or equal to the next epoch, eligibility is 0% + if (entryRound >= nextPayoutRound) { return 0 } - // Calculate rewards eligibility as a percentage entry round vs epoch beginning - const timeInEpoch = nextEpoch - entryRound - let eligibilityPercent = (timeInEpoch / epochRoundLength) * 100 - // Ensure eligibility falls within 0-100% range - // If eligibility is negative, it means they're past the epoch (entry time + 320 rounds, ~16 mins) - eligibilityPercent = Math.max(0, Math.min(eligibilityPercent, 100)) + // Calculate the effective rounds staked within the current epoch starting from the last payout + const roundsInEpoch = Math.max(0, lastPoolPayoutRound - entryRound) + + // Calculate eligibility as a percentage of the epoch length + const eligibilePercent = (roundsInEpoch / epochRoundLength) * 100 + + // Ensure eligibility is within 0-100% range + const rewardEligibility = Math.max(0, Math.min(eligibilePercent, 100)) - // Round down to nearest integer - return Math.floor(eligibilityPercent) + // Round down to the nearest integer + return Math.floor(rewardEligibility) } /** diff --git a/ui/src/utils/convert.spec.ts b/ui/src/utils/convert.spec.ts new file mode 100644 index 00000000..0375011c --- /dev/null +++ b/ui/src/utils/convert.spec.ts @@ -0,0 +1,64 @@ +import { ToStringTypes } from '@/interfaces/utils' +import { convertToStringTypes } from '@/utils/convert' + +describe('convertToStringTypes', () => { + it('should convert an object with primitive values to strings', () => { + const input = { + number: 123, + boolean: true, + string: 'hello', + } + const expected: ToStringTypes = { + number: '123', + boolean: 'true', + string: 'hello', + } + const result = convertToStringTypes(input) + expect(result).toEqual(expected) + }) + + it('should convert an object with arrays of primitive values to strings', () => { + const input = { + numbers: [1, 2, 3], + booleans: [true, false, true], + strings: ['apple', 'banana'], + } + const expected: ToStringTypes = { + numbers: ['1', '2', '3'], + booleans: ['true', 'false', 'true'], + strings: ['apple', 'banana'], + } + const result = convertToStringTypes(input) + expect(result).toEqual(expected) + }) + + it('should handle empty objects and arrays', () => { + const input = { + emptyArray: [], + emptyObject: {}, + } + const expected: ToStringTypes = { + emptyArray: [], + emptyObject: '{}', + } + const result = convertToStringTypes(input) + expect(result).toEqual(expected) + }) + + it('should convert nested arrays to strings', () => { + const input = { + nestedArray: [ + [1, 2], + [3, 4], + ], + } + const expected: ToStringTypes = { + nestedArray: [ + ['1', '2'], + ['3', '4'], + ], + } + const result = convertToStringTypes(input) + expect(result).toEqual(expected) + }) +}) diff --git a/ui/src/utils/convert.ts b/ui/src/utils/convert.ts index 01933149..a055c4fc 100644 --- a/ui/src/utils/convert.ts +++ b/ui/src/utils/convert.ts @@ -1,17 +1,52 @@ import { ToStringTypes } from '@/interfaces/utils' +/** + * Converts all values in an object to strings. + * Handles nested objects and arrays. + * @template T - The type of the object to convert + * @param {T} obj - The object to convert + * @returns {ToStringTypes} The object with all values converted to strings + * @example + * const input = { + * number: 123, + * boolean: true, + * string: 'hello', + * array: [1, 2, 3], + * } + * const result = convertToStringTypes(input) + * // result = { + * // number: '123', + * // boolean: 'true', + * // string: 'hello', + * // array: ['1', '2', '3'], + * // } + */ export function convertToStringTypes(obj: T): ToStringTypes { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {} + for (const key in obj) { const value = obj[key] + if (Array.isArray(value)) { - // For arrays and tuples convert each element to a string - result[key] = value.map(String) + // Handle arrays + result[key] = value.map((element) => { + if (Array.isArray(element)) { + // Maintain nested array structure + return element.map((subElement) => String(subElement)) + } else if (typeof element === 'object' && element !== null) { + // Recursively convert non-array objects + return convertToStringTypes(element) + } + return String(element) + }) + } else if (typeof value === 'object' && value !== null) { + // Recursively convert non-array objects + result[key] = Object.keys(value).length === 0 ? '{}' : convertToStringTypes(value) } else { - // Convert non-array values to strings result[key] = String(value) } } + return result as ToStringTypes } diff --git a/ui/src/utils/dayjs.spec.ts b/ui/src/utils/dayjs.spec.ts new file mode 100644 index 00000000..8cf05b5c --- /dev/null +++ b/ui/src/utils/dayjs.spec.ts @@ -0,0 +1,44 @@ +import { formatDuration } from '@/utils/dayjs' + +describe('formatDuration', () => { + it('should format a duration of less than a minute correctly', () => { + const milliseconds = 45000 // 45 seconds + const result = formatDuration(milliseconds) + expect(result).toBe('45s') + }) + + it('should format a duration of more than a minute but less than an hour correctly', () => { + const milliseconds = 600000 // 10 minutes + const result = formatDuration(milliseconds) + expect(result).toBe('10m') + }) + + it('should format a duration of more than an hour but less than a day correctly', () => { + const milliseconds = 5400000 // 1 hour 30 minutes + const result = formatDuration(milliseconds) + expect(result).toBe('1h 30m') + }) + + it('should format a duration of multiple days correctly', () => { + const milliseconds = 181800000 // 2 days 2 hours 30 minutes + const result = formatDuration(milliseconds) + expect(result).toBe('2d 2h 30m') + }) + + it('should format a duration with all units correctly', () => { + const milliseconds = 123456789 // 1 day 10 hours 17 minutes 36 seconds + const result = formatDuration(milliseconds) + expect(result).toBe('1d 10h 17m 36s') + }) + + it('should handle a zero duration correctly', () => { + const result = formatDuration(0) + expect(result).toBe('') + }) + + it('should not include zero-valued components in the output', () => { + const milliseconds = 3600000 // 1 hour + const result = formatDuration(milliseconds) + expect(result).toBe('1h') + }) +}) diff --git a/ui/src/utils/dayjs.ts b/ui/src/utils/dayjs.ts index 35d4d638..439b0ebb 100644 --- a/ui/src/utils/dayjs.ts +++ b/ui/src/utils/dayjs.ts @@ -5,6 +5,13 @@ import localizedFormat from 'dayjs/plugin/localizedFormat' dayjs.extend(duration) dayjs.extend(localizedFormat) +/** + * Format a duration in milliseconds as a human-readable string. + * @param {number} milliseconds - The duration in milliseconds + * @returns {string} The formatted duration + * @example + * formatDuration(123456789) // '1d 10h 17m 36s' + */ export function formatDuration(milliseconds: number): string { const dur = dayjs.duration(milliseconds) diff --git a/ui/src/utils/ellipseAddress.spec.tsx b/ui/src/utils/ellipseAddress.spec.tsx index 2cbff100..58c0589c 100644 --- a/ui/src/utils/ellipseAddress.spec.tsx +++ b/ui/src/utils/ellipseAddress.spec.tsx @@ -1,4 +1,5 @@ -import { ellipseAddress } from './ellipseAddress' +import { render } from '@testing-library/react' +import { ellipseAddress, ellipseAddressJsx } from '@/utils/ellipseAddress' describe('ellipseAddress', () => { it('should return ellipsed address with specified width', () => { @@ -13,3 +14,22 @@ describe('ellipseAddress', () => { expect(result).toBe('') }) }) + +describe('ellipseAddressJsx', () => { + it('should return ellipsed address with specified width', () => { + const address = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const width = 4 + + const { container } = render(ellipseAddressJsx(address, width)) + + expect(container).toHaveTextContent('aaaa…aaaa') + }) + + it('should return an empty string when the address is empty', () => { + const address = '' + + const { container } = render(ellipseAddressJsx(address)) + + expect(container.childNodes).toHaveLength(0) + }) +}) diff --git a/ui/src/utils/ellipseAddress.tsx b/ui/src/utils/ellipseAddress.tsx index 3b211463..c0bce107 100644 --- a/ui/src/utils/ellipseAddress.tsx +++ b/ui/src/utils/ellipseAddress.tsx @@ -1,8 +1,26 @@ -export function ellipseAddress(address = ``, width = 6): string { +/** + * Ellipsize an Algorand address with `...` (returns text only) + * @param {string} address - The address to ellipsize + * @param {number} width - The number of characters to display on each side of the ellipsis (default: 6) + * @returns {string} The ellipsized address + * @example + * ellipseAddress('7IQQUVXUJHQ4CQSDFTYEZWEWNQZWAMCQAEJPFBZCGPOPOSJ7YZZCOH25GE') + * // '7IQQUV...COH25GE' + */ +export function ellipseAddress(address: string = '', width: number = 6): string { return address ? `${address.slice(0, width)}...${address.slice(-width)}` : address } -export function ellipseAddressJsx(address = ``, width = 6): JSX.Element { +/** + * Ellipsize an Algorand address with `…` HTML entity (returns JSX) + * @param {string} address - The address to ellipsize + * @param {number} width - The number of characters to display on each side of the ellipsis (default: 6) + * @returns {JSX.Element} The ellipsized address + * @example + * ellipseAddressJsx('7IQQUVXUJHQ4CQSDFTYEZWEWNQZWAMCQAEJPFBZCGPOPOSJ7YZZCOH25GE') + * // 7IQQUV…COH25GE + */ +export function ellipseAddressJsx(address: string = '', width: number = 6): JSX.Element { return address ? ( <> {address.slice(0, width)}…{address.slice(-width)} diff --git a/ui/src/utils/explorer.spec.ts b/ui/src/utils/explorer.spec.ts new file mode 100644 index 00000000..8bfd0390 --- /dev/null +++ b/ui/src/utils/explorer.spec.ts @@ -0,0 +1,42 @@ +import { ExplorerLink } from '@/utils/explorer' + +const mockConfig = { + accountUrl: 'https://mock-explorer.com/account', + transactionUrl: 'https://mock-explorer.com/transaction', + assetUrl: 'https://mock-explorer.com/asset', + appUrl: 'https://mock-explorer.com/app', +} + +vi.mock('@/utils/network/getExplorerConfig', () => ({ + getExplorerConfigFromViteEnvironment: vi.fn(() => mockConfig), +})) + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('ExplorerLink', () => { + it('should generate correct account URL using static method', () => { + const address = 'STATIC_ACCOUNT' + const expectedUrl = `${mockConfig.accountUrl}/${address}` + expect(ExplorerLink.account(address)).toBe(expectedUrl) + }) + + it('should generate correct transaction URL using static method', () => { + const id = 'STATIC_TX' + const expectedUrl = `${mockConfig.transactionUrl}/${id}` + expect(ExplorerLink.tx(id)).toBe(expectedUrl) + }) + + it('should generate correct asset URL using static method', () => { + const id = 12345 + const expectedUrl = `${mockConfig.assetUrl}/${id}` + expect(ExplorerLink.asset(id)).toBe(expectedUrl) + }) + + it('should generate correct app URL using static method', () => { + const id = 67890 + const expectedUrl = `${mockConfig.appUrl}/${id}` + expect(ExplorerLink.app(id)).toBe(expectedUrl) + }) +}) diff --git a/ui/src/utils/explorer.ts b/ui/src/utils/explorer.ts index f2e4ce11..e8c28419 100644 --- a/ui/src/utils/explorer.ts +++ b/ui/src/utils/explorer.ts @@ -7,24 +7,24 @@ export class ExplorerLink { private config: ExplorerConfig private identifier: string - constructor(identifier: string) { + private constructor(identifier: string) { this.config = getExplorerConfigFromViteEnvironment() this.identifier = identifier } - accountUrl() { + private accountUrl() { return `${this.config.accountUrl}/${this.identifier}` } - transactionUrl() { + private transactionUrl() { return `${this.config.transactionUrl}/${this.identifier}` } - assetUrl() { + private assetUrl() { return `${this.config.assetUrl}/${this.identifier}` } - appUrl() { + private appUrl() { return `${this.config.appUrl}/${this.identifier}` } diff --git a/ui/src/utils/format.spec.ts b/ui/src/utils/format.spec.ts index 7171342e..c68fb51e 100644 --- a/ui/src/utils/format.spec.ts +++ b/ui/src/utils/format.spec.ts @@ -1,4 +1,92 @@ -import { formatNumber } from '@/utils/format' +import { + convertFromBaseUnits, + convertToBaseUnits, + formatAlgoAmount, + formatAssetAmount, + formatBigIntWithCommas, + formatNumber, + formatWithPrecision, + roundToFirstNonZeroDecimal, +} from '@/utils/format' + +describe('convertFromBaseUnits', () => { + it('should convert from base units correctly', () => { + expect(convertFromBaseUnits(1000000, 6)).toBe(1) + expect(convertFromBaseUnits(1234567, 3)).toBe(1234.567) + }) + + it('should handle zero decimals correctly', () => { + expect(convertFromBaseUnits(12345, 0)).toBe(12345) + }) +}) + +describe('convertToBaseUnits', () => { + it('should convert to base units correctly', () => { + expect(convertToBaseUnits(1, 6)).toBe(1000000) + expect(convertToBaseUnits(1234.567, 3)).toBe(1234567) + }) + + it('should handle zero decimals correctly', () => { + expect(convertToBaseUnits(12345, 0)).toBe(12345) + }) +}) + +describe('formatWithPrecision', () => { + it('should format a number with precision and suffixes, trimming zeros', () => { + expect(formatWithPrecision(1e12, 3)).toBe('1T') + expect(formatWithPrecision(2.345e12, 1)).toBe('2.3T') + expect(formatWithPrecision(3.45678e9, 2)).toBe('3.46B') + expect(formatWithPrecision(4.56789e6, 3)).toBe('4.568M') + expect(formatWithPrecision(1234.567, 2)).toBe('1.23K') + }) +}) + +describe('formatAssetAmount', () => { + it('should format asset amounts correctly', () => { + expect(formatAssetAmount(1234567, true, 6)).toBe('1.234567') + expect(formatAssetAmount(1000, false, 0)).toBe('1,000') + }) + + it('should trim trailing zeros when trim is true', () => { + expect(formatAssetAmount(1000, false, 6, true)).toBe('1,000') + expect(formatAssetAmount(1234.56789, false, 6, true)).toBe('1,234.56789') + }) + + it('should retain trailing zeros when trim is false', () => { + expect(formatAssetAmount(1000, false, 6, false)).toBe('1,000.000000') + }) + + it('should handle an invalid amount gracefully', () => { + expect(formatAssetAmount('abc', true, 6)).toBe('NaN') + }) + + it('should apply the maxLength option', () => { + expect(formatAssetAmount(1234567890, false, 2, true, 6)).toBe('1.2B') + expect(formatAssetAmount(1000000, false, 6, true, 5)).toBe('1M') + expect(formatAssetAmount(987654321, false, 3, true, 8)).toBe('987.7M') + }) +}) + +describe('formatAlgoAmount', () => { + it('should format Algorand amounts correctly', () => { + expect(formatAlgoAmount(1234567, true)).toBe('1.234567') + expect(formatAlgoAmount(1000000, true)).toBe('1') + }) +}) + +describe('roundToFirstNonZeroDecimal', () => { + it('should round to the first non-zero decimal', () => { + expect(roundToFirstNonZeroDecimal(0.001234)).toBe(0.001) + expect(roundToFirstNonZeroDecimal(0.0005678)).toBe(0.0006) + expect(roundToFirstNonZeroDecimal(1234.567)).toBe(1234.567) + }) +}) + +describe('formatBigIntWithCommas', () => { + it('should format a BigInt with commas', () => { + expect(formatBigIntWithCommas(12345678901234567890n)).toBe('12,345,678,901,234,567,890') + }) +}) describe('formatNumber', () => { it('should format a large number with commas', () => { diff --git a/ui/src/utils/format.ts b/ui/src/utils/format.ts index ba422bce..722afc5e 100644 --- a/ui/src/utils/format.ts +++ b/ui/src/utils/format.ts @@ -1,24 +1,54 @@ import Big from 'big.js' -export type RoundingMode = 'roundDown' | 'roundUp' | 'roundHalfUp' | 'roundHalfEven' - -export function convertFromBaseUnits(amount: number, decimals = 0, rm: RoundingMode = 'roundDown') { +/** + * Convert an asset amount from base units to whole units + * @param {number} amount - The amount in base units + * @param {number} decimals - The number of decimal places + * @returns {number} The amount in whole units + * @example + * convertFromBaseUnits(12345, 0) // 12345 + * convertFromBaseUnits(12345, 6) // 0.012345 + * convertFromBaseUnits(1000000, 6) // 1 + */ +export function convertFromBaseUnits(amount: number, decimals: number = 0): number { if (decimals === 0) return amount const divisor = new Big(10).pow(decimals) - const baseUnits = new Big(amount).round(decimals, Big[rm]) - return baseUnits.div(divisor).toNumber() + return new Big(amount).div(divisor).toNumber() } -export function convertToBaseUnits(amount: number, decimals = 0, rm: RoundingMode = 'roundDown') { +/** + * Convert an asset amount from whole units to base units + * @param {number} amount - The amount in whole units + * @param {number} decimals - The number of decimal places + * @returns {number} The amount in base units + * @example + * convertToBaseUnits(1, 6) // 1000000 + * convertToBaseUnits(0.012345, 6) // 12345 + * convertToBaseUnits(12345, 0) // 12345 + */ +export function convertToBaseUnits(amount: number, decimals: number = 0): number { if (decimals === 0) return amount const multiplier = new Big(10).pow(decimals) - const wholeUnits = new Big(amount).round(decimals, Big[rm]) - return wholeUnits.times(multiplier).toNumber() + return new Big(amount).times(multiplier).toNumber() } -export function formatWithPrecision(num: number, precision: number) { +/** + * Format a number with precision and suffixes + * @param {number} num - The number to format + * @param {number} precision - The number of decimal places + * @returns {string} The formatted number with precision and suffixes + * @example + * formatWithPrecision(1e12, 3) // '1T' + * formatWithPrecision(2.345e12, 1) // '2.3T' + * formatWithPrecision(3.45678e9, 2) // '3.46B' + * formatWithPrecision(4.56789e6, 3) // '4.568M' + * formatWithPrecision(1234.567, 2) // '1.23K' + */ +export function formatWithPrecision(num: number, precision: number): string { let scaledNum = num let suffix = '' + + // Determine the appropriate suffix and scale the number if (num >= 1e12) { suffix = 'T' scaledNum = num / 1e12 @@ -32,14 +62,35 @@ export function formatWithPrecision(num: number, precision: number) { suffix = 'K' scaledNum = num / 1e3 } - return scaledNum.toFixed(precision) + suffix + + // Format the number with precision and trim trailing zeros + const formattedNumber = scaledNum.toFixed(precision).replace(/\.?0+$/, '') + + return formattedNumber + suffix } +// @todo: Convert options to an object +/** + * Format an asset amount with commas and optional decimal places + * @param {number | string} amount - The asset amount to format + * @param {boolean} baseUnits - Whether the amount is in base units + * @param {number} decimals - The number of decimal places + * @param {boolean} trim - Whether to trim trailing zeros + * @param {number} maxLength - The maximum length of the formatted string + * @returns {string} The formatted asset amount + * @example + * formatAssetAmount(1234567, true, 6) // '1.234567' + * formatAssetAmount(1000, false, 0) // '1,000' + * formatAssetAmount(1234.56789, false, 6, true) // '1,234.56789' + * formatAssetAmount(1000, false, 6, false) // '1,000.000000' + * formatAssetAmount('abc', true, 6) // 'NaN' + * formatAssetAmount(1234.56789, false, 6, true, 10) // '1.2K' + */ export function formatAssetAmount( amount: number | string, - baseUnits = false, - decimals = 6, - trim = true, + baseUnits: boolean = false, + decimals: number = 6, + trim: boolean = true, maxLength?: number, ): string { // If amount is a string, parse it to a number @@ -72,19 +123,42 @@ export function formatAssetAmount( return parts.join('.') } +// @todo: Convert options to an object +/** + * Format an Algo amount with commas and optional decimal places + * @param {number | string} amount - The Algo amount to format + * @param {boolean} microalgos - Whether the amount is in microalgos + * @param {boolean} trim - Whether to trim trailing zeros + * @param {number} maxLength - The maximum length of the formatted string + * @returns {string} The formatted Algo amount + * @example + * formatAlgoAmount(1234567, true) // '1.234567' + * formatAlgoAmount(1000, false) // '1,000' + * formatAlgoAmount(1234.56789, false, true) // '1,234.56789' + * formatAlgoAmount(1000, false, false) // '1,000.000000' + */ export function formatAlgoAmount( amount: number | string, - microalgos = false, - trim = true, + microalgos: boolean = false, + trim: boolean = true, maxLength?: number, ): string { return formatAssetAmount(amount, microalgos, 6, trim, maxLength) } +/** + * Round a number to the first non-zero decimal place + * @param {number} num - The number to round + * @returns {number} The rounded number + * @example + * roundToFirstNonZeroDecimal(0.001234) // 0.001 + * roundToFirstNonZeroDecimal(0.0005678) // 0.0006 + * roundToFirstNonZeroDecimal(1234.567) // 1234.567 + */ export function roundToFirstNonZeroDecimal(num: number): number { if (num === 0) return 0 - // Convert the number to exponential format to easily find the exponent + // Convert the number to exponential format to find the exponent const expForm = num.toExponential().split('e') const exponent = parseInt(expForm[1]) @@ -98,7 +172,9 @@ export function roundToFirstNonZeroDecimal(num: number): number { /** * Format a BigInt with commas * @param {bigint} value - The BigInt value - * @returns {string} - The formatted BigInt value with commas + * @returns {string} The formatted BigInt value with commas + * @example + * formatBigIntWithCommas(12345678901234567890n) // '12,345,678,901,234,567,890' */ export function formatBigIntWithCommas(value: bigint): string { const valueStr = value.toString() @@ -116,7 +192,19 @@ type FormatNumberOptions = { * Format a number with commas and optional decimal places * @param {number | bigint | string} amount - The number to format * @param {FormatNumberOptions} options - Options for formatting the number - * @returns {string} - The formatted number + * @param {boolean} options.compact - Whether to format the number in compact notation + * @param {number} options.precision - The number of decimal places + * @param {boolean} options.trim - Whether to trim trailing zeros + * @returns {string} The formatted number + * @example + * formatNumber(1234567890) // '1,234,567,890' + * formatNumber(12345.6789, { precision: 2 }) // '12,345.68' + * formatNumber(1234567, { compact: true, precision: 2 }) // '1.23M' + * formatNumber(12345678901234567890n) // '12,345,678,901,234,567,890' + * formatNumber('987654321.1234', { precision: 3 }) // '987,654,321.123' + * formatNumber(100.5, { precision: 3, trim: true }) // '100.5' + * formatNumber(100.5, { precision: 3, trim: false }) // '100.500' + * formatNumber(-9876543.21, { precision: 2 }) // '-9,876,543.21' */ export function formatNumber( amount: number | bigint | string, diff --git a/ui/src/utils/nfd.spec.ts b/ui/src/utils/nfd.spec.ts new file mode 100644 index 00000000..1d1746ff --- /dev/null +++ b/ui/src/utils/nfd.spec.ts @@ -0,0 +1,104 @@ +import { Nfd } from '@/interfaces/nfd' +import { + getNfdAvatarUrl, + getNfdProfileUrl, + isValidName, + isValidRoot, + isValidSegment, + trimExtension, + trimSegment, +} from '@/utils/nfd' +import { MOCK_ROOT_NFD as mockNfd } from '@/utils/tests/fixtures/nfd' + +const mockBaseUrl = 'https://nfd-app.mock' + +// Mock getNfdConfig +vi.mock('@/utils/network/getNfdConfig', () => ({ + getNfdAppFromViteEnvironment: vi.fn(() => mockBaseUrl), +})) + +describe('isValidName', () => { + it('should validate NFD names correctly', () => { + expect(isValidName('example.algo')).toBe(true) + expect(isValidName('example', true)).toBe(true) + expect(isValidName('invalid_name.algo')).toBe(false) + }) +}) + +describe('isValidRoot', () => { + it('should validate NFD roots correctly', () => { + expect(isValidRoot('root.algo')).toBe(true) + expect(isValidRoot('root', true)).toBe(true) + expect(isValidRoot('invalid_root')).toBe(false) + }) +}) + +describe('isValidSegment', () => { + it('should validate NFD segments correctly', () => { + expect(isValidSegment('segment.root.algo')).toBe(true) + expect(isValidSegment('segment.root', true)).toBe(true) + expect(isValidSegment('invalid_segment')).toBe(false) + }) +}) + +describe('trimExtension', () => { + it('should trim the .algo suffix', () => { + expect(trimExtension('example.algo')).toBe('example') + expect(trimExtension('root')).toBe('root') + }) +}) + +describe('trimSegment', () => { + it('should trim the segment prefix', () => { + expect(trimSegment('segment.root.algo')).toBe('root.algo') + expect(trimSegment('root.algo')).toBe('root.algo') + expect(trimSegment('invalid_segment')).toBe('invalid_segment') + }) +}) + +describe('getNfdProfileUrl', () => { + it('should generate the correct profile URL', () => { + expect(getNfdProfileUrl('example.algo')).toBe(`${mockBaseUrl}/name/example.algo`) + }) +}) + +describe('getNfdAvatarUrl', () => { + it('should return a curated placeholder for curated NFDs', () => { + const curatedNfd: Nfd = { + ...mockNfd, + category: 'curated', + properties: { verified: {}, userDefined: {} }, + } + expect(getNfdAvatarUrl(curatedNfd)).toBe(`${mockBaseUrl}/img/nfd-image-placeholder_gold.jpg`) + }) + + it('should return a gray placeholder for available, forSale, or reserved NFDs', () => { + expect(getNfdAvatarUrl({ ...mockNfd, state: 'available' })).toBe( + `${mockBaseUrl}/img/nfd-image-placeholder_gray.jpg`, + ) + expect(getNfdAvatarUrl({ ...mockNfd, state: 'forSale' })).toBe( + `${mockBaseUrl}/img/nfd-image-placeholder_gray.jpg`, + ) + expect(getNfdAvatarUrl({ ...mockNfd, state: 'reserved' })).toBe( + `${mockBaseUrl}/img/nfd-image-placeholder_gray.jpg`, + ) + }) + + it('should return a generic placeholder for other cases without an avatar', () => { + expect( + getNfdAvatarUrl({ + ...mockNfd, + state: 'owned', + properties: { verified: {}, userDefined: {} }, + }), + ).toBe(`${mockBaseUrl}/img/nfd-image-placeholder.jpg`) + }) + + it('should return the provided avatar URL if it exists', () => { + const nfdWithAvatar: Nfd = { + ...mockNfd, + properties: { verified: {}, userDefined: { avatar: 'https://mock-avatar.com/avatar.png' } }, + } + expect(getNfdAvatarUrl(nfdWithAvatar)).toBe('https://mock-avatar.com/avatar.png') + }) +}) diff --git a/ui/src/utils/nfd.ts b/ui/src/utils/nfd.ts index d0e26783..3a73bc06 100644 --- a/ui/src/utils/nfd.ts +++ b/ui/src/utils/nfd.ts @@ -2,12 +2,16 @@ import { Nfd } from '@/interfaces/nfd' import { getNfdAppFromViteEnvironment } from '@/utils/network/getNfdConfig' /** - * @description Checks if name is a valid NFD root/segment - * @param {string} name NFD name to validate - * @param {boolean} suffixOptional if true, '.algo' suffix is optional (default: false) - * @returns {boolean} true if valid + * Checks if name is a valid NFD name + * @param {string} name - The NFD name to validate + * @param {boolean} suffixOptional - Whether the '.algo' suffix is optional (default: false) + * @returns {boolean} True if valid + * @example + * isValidName('example.algo') // true + * isValidName('example', true) // true + * isValidName('invalid_name.algo') // false */ -export function isValidName(name: string, suffixOptional = false): boolean { +export function isValidName(name: string, suffixOptional: boolean = false): boolean { if (suffixOptional) { return /^([a-z0-9]{1,27}\.){0,1}(?[a-z0-9]{1,27})(\.algo)?$/g.test(name) } @@ -15,12 +19,16 @@ export function isValidName(name: string, suffixOptional = false): boolean { } /** - * @description Checks if name is a valid NFD root - * @param {string} name NFD name to validate - * @param {boolean} suffixOptional if true, '.algo' suffix is optional (default: false) - * @returns {boolean} true if valid + * Checks if name is a valid NFD root + * @param {string} name - The NFD root to validate + * @param {boolean} suffixOptional - Whether the '.algo' suffix is optional (default: false) + * @returns {boolean} True if valid + * @example + * isValidRoot('root.algo') // true + * isValidRoot('root', true) // true + * isValidRoot('invalid_root') // false */ -export const isValidRoot = (name: string, suffixOptional = false): boolean => { +export const isValidRoot = (name: string, suffixOptional: boolean = false): boolean => { if (suffixOptional) { return /^[a-z0-9]{1,27}(\.algo)?$/g.test(name) } @@ -28,12 +36,16 @@ export const isValidRoot = (name: string, suffixOptional = false): boolean => { } /** - * @description Checks if name is a valid NFD segment - * @param {string} name NFD name to validate - * @param {boolean} suffixOptional if true, '.algo' suffix is optional (default: false) - * @returns {boolean} true if valid + * Checks if name is a valid NFD segment + * @param {string} name - The NFD segment to validate + * @param {boolean} suffixOptional - Whether the '.algo' suffix is optional (default: false) + * @returns {boolean} True if valid + * @example + * isValidSegment('segment.root.algo') // true + * isValidSegment('segment.root', true) // true + * isValidSegment('invalid_segment') // false */ -export const isValidSegment = (name: string, suffixOptional = false): boolean => { +export const isValidSegment = (name: string, suffixOptional: boolean = false): boolean => { if (suffixOptional) { return /^[a-z0-9]{1,27}\.(?[a-z0-9]{1,27})(\.algo)?$/g.test(name) } @@ -41,18 +53,25 @@ export const isValidSegment = (name: string, suffixOptional = false): boolean => } /** - * @description Trims the '.algo' suffix from the provided NFD, if it exists - * @param {string} str NFD name to trim + * Trims the '.algo' suffix from the provided name + * @param {string} str - The NFD name to trim * @returns {string} NFD name with suffix removed + * @example + * trimExtension('example.algo') // 'example' + * trimExtension('root') // 'root' */ export const trimExtension = (str: string): string => { return str.replace(/\.algo$/gi, '') } /** - * @description Trims the segment prefix from the provided NFD, if it exists - * @param {string} str NFD name to trim - * @returns {string} NFD name with prefix removed, or original string if invalid + * Trims the segment prefix from the provided name + * @param {string} str - The NFD name to trim + * @returns {string} NFD name with segment prefix removed + * @example + * trimSegment('segment.root.algo') // 'root.algo' + * trimSegment('root.algo') // 'root.algo' + * trimSegment('invalid_segment') // 'invalid_segment' */ export const trimSegment = (str: string): string => { if (!isValidName(str)) { @@ -61,11 +80,26 @@ export const trimSegment = (str: string): string => { return str.match(/^[a-z0-9]{1,27}\.algo$/gi) ? str : `${str.split('.')[1]}.algo` } +/** + * Generates the NFD profile URL for the provided name. + * @param {string} name - The NFD name to generate the URL for + * @returns {string} The NFD profile URL + * @example + * getNfdProfileUrl('example.algo') // 'https://nfd-app.mock/name/example.algo' + */ export function getNfdProfileUrl(name: string): string { const baseUrl = getNfdAppFromViteEnvironment() return `${baseUrl}/name/${name}` } +/** + * Generates the NFD avatar URL for the provided NFD. + * The base URL must be set as VITE_NFD_APP_URL in the Vite environment. + * @param {Nfd} nfd - The NFD to generate the URL for + * @returns {string} The NFD avatar URL + * @example + * getNfdAvatarUrl(nfd) // 'https://app.nf.domains/img/nfd-image-placeholder.jpg' + */ export const getNfdAvatarUrl = (nfd: Nfd): string => { const baseUrl = getNfdAppFromViteEnvironment() const url = nfd?.properties?.userDefined?.avatar || nfd?.properties?.verified?.avatar diff --git a/ui/src/utils/paramsCache.spec.ts b/ui/src/utils/paramsCache.spec.ts new file mode 100644 index 00000000..b7acfc06 --- /dev/null +++ b/ui/src/utils/paramsCache.spec.ts @@ -0,0 +1,68 @@ +import * as algokit from '@algorandfoundation/algokit-utils' +import algosdk from 'algosdk' +import { ParamsCache } from '@/utils/paramsCache' + +const mockParams: algosdk.SuggestedParams = { + fee: 1000, + firstRound: 1000, + lastRound: 2000, + genesisID: 'dockernet-v1', + genesisHash: 'v1lkQZYrxQn1XDRkIAlsUrSSECXU6OFMbPMhj/QQ9dk=', +} + +// Mock getTransactionParams +const mockGetTransactionParams = vi.fn().mockResolvedValue(mockParams) + +// Mock algokit-utils +vi.mock('@algorandfoundation/algokit-utils', () => ({ + getAlgoClient: vi.fn(() => ({ + getTransactionParams: vi.fn(() => ({ + do: mockGetTransactionParams, + })), + })), +})) + +// Mock getAlgodConfigFromViteEnvironment +vi.mock('@/utils/network/getAlgoClientConfigs', () => ({ + getAlgodConfigFromViteEnvironment: () => ({ + server: 'http://localhost', + port: '4001', + token: '', + }), +})) + +beforeEach(() => { + vi.clearAllMocks() + ParamsCache.resetInstance() // Reset singleton instance +}) + +describe('ParamsCache', () => { + it('should fetch and cache transaction parameters', async () => { + const params = await ParamsCache.getSuggestedParams() + expect(algokit.getAlgoClient).toHaveBeenCalledTimes(1) // First call + expect(params).toEqual(mockParams) + + // Simulate another call within 5 minutes + const cachedParams = await ParamsCache.getSuggestedParams() + expect(algokit.getAlgoClient).toHaveBeenCalledTimes(1) // No second call (cached) + expect(cachedParams).toEqual(params) + }) + + it('should refresh cached parameters after expiration', async () => { + const initialParams = await ParamsCache.getSuggestedParams() + expect(algokit.getAlgoClient).toHaveBeenCalledTimes(1) // First call + expect(initialParams).toEqual(mockParams) + + // Mock Date.now to simulate cache expiration (5 minutes later) + const originalNow = Date.now + global.Date.now = () => originalNow() + 1000 * 60 * 6 // +6 minutes + + // Simulate another call after expiration + const refreshedParams = await ParamsCache.getSuggestedParams() + expect(mockGetTransactionParams).toHaveBeenCalledTimes(2) // Second call + expect(refreshedParams).toEqual(initialParams) + + // Restore Date.now + global.Date.now = originalNow + }) +}) diff --git a/ui/src/utils/paramsCache.ts b/ui/src/utils/paramsCache.ts index c892fa20..2ea37180 100644 --- a/ui/src/utils/paramsCache.ts +++ b/ui/src/utils/paramsCache.ts @@ -2,19 +2,27 @@ import * as algokit from '@algorandfoundation/algokit-utils' import algosdk from 'algosdk' import { getAlgodConfigFromViteEnvironment } from '@/utils/network/getAlgoClientConfigs' -const algodConfig = getAlgodConfigFromViteEnvironment() - interface CachedParams { suggestedParams: algosdk.SuggestedParams timestamp: number } +/** + * This singleton class should be used to fetch suggested transaction parameters. + * It will cache the parameters for 5 minutes to avoid refetching for every transaction. + * @method getSuggestedParams - Static method to fetch suggested transaction parameters + * @returns {Promise} Suggested transaction parameters + * @example + * const suggestedParams = await ParamsCache.getSuggestedParams() + * @see {@link https://developer.algorand.org/docs/rest-apis/algod/#get-v2transactionsparams} + */ export class ParamsCache { - private static readonly instance: ParamsCache = new ParamsCache() + private static instance: ParamsCache | null = null private client: algosdk.Algodv2 private cache: CachedParams | null = null private constructor() { + const algodConfig = getAlgodConfigFromViteEnvironment() this.client = algokit.getAlgoClient({ server: algodConfig.server, port: algodConfig.port, @@ -23,6 +31,9 @@ export class ParamsCache { } public static async getSuggestedParams(): Promise { + if (!this.instance) { + this.instance = new ParamsCache() + } return this.instance.fetchAndCacheParams() } @@ -41,4 +52,9 @@ export class ParamsCache { } return suggestedParams } + + // Reset instance for testing purposes + public static resetInstance() { + this.instance = null + } } diff --git a/ui/src/utils/tests/abi.ts b/ui/src/utils/tests/abi.ts index a015babc..6ead567e 100644 --- a/ui/src/utils/tests/abi.ts +++ b/ui/src/utils/tests/abi.ts @@ -1,4 +1,4 @@ -import { ABITupleType, ABIValue } from 'algosdk' +import { ABIValue } from 'algosdk' import { StakingPoolSig } from '@/contracts/StakingPoolClient' import { ValidatorRegistrySig } from '@/contracts/ValidatorRegistryClient' @@ -7,6 +7,16 @@ export interface MethodCallParams { args?: Record } +/** + * Encodes an ABI method call's parameters (name and args) into a Uint8Array. + * It is used to pass the parameters in the note field of its transaction. + * In testing, a MSW endpoint handler can parse these parameters to return the correct mock response. + * @param {string} method - The method name + * @param {Record} args - The method arguments + * @returns {Uint8Array} The encoded method call parameters + * @example + * encodeCallParams('getPools', { validatorId: 1 }) + */ export function encodeCallParams( method: ValidatorRegistrySig | StakingPoolSig, args: MethodCallParams['args'], @@ -15,45 +25,3 @@ export function encodeCallParams( const callParams: MethodCallParams = { method: methodName, ...(args ? { args } : {}) } return new Uint8Array(Buffer.from(JSON.stringify(callParams))) } - -export function parseMethodSignature(signature: string): { - name: string - args: string[] - returns: string -} { - const argsStart = signature.indexOf('(') - if (argsStart === -1) { - throw new Error(`Invalid method signature: ${signature}`) - } - - let argsEnd = -1 - let depth = 0 - for (let i = argsStart; i < signature.length; i++) { - const char = signature[i] - - if (char === '(') { - depth += 1 - } else if (char === ')') { - if (depth === 0) { - // unpaired parenthesis - break - } - - depth -= 1 - if (depth === 0) { - argsEnd = i - break - } - } - } - - if (argsEnd === -1) { - throw new Error(`Invalid method signature: ${signature}`) - } - - return { - name: signature.slice(0, argsStart), - args: ABITupleType.parseTupleContent(signature.slice(argsStart + 1, argsEnd)), - returns: signature.slice(argsEnd + 1), - } -} diff --git a/ui/src/utils/tests/fixtures/applications.ts b/ui/src/utils/tests/fixtures/applications.ts index 1578e30c..cd2b8c2c 100644 --- a/ui/src/utils/tests/fixtures/applications.ts +++ b/ui/src/utils/tests/fixtures/applications.ts @@ -5,7 +5,9 @@ interface FixtureData { [appId: string]: Application } -// Map containing each app's corresponding application fixture data +/** + * Map containing each application's fixture data + */ export const appFixtures: FixtureData = { '1010': { // Staking pool appId 1010 diff --git a/ui/src/utils/tests/fixtures/boxes.ts b/ui/src/utils/tests/fixtures/boxes.ts index 5ff92ea6..3c9e4450 100644 --- a/ui/src/utils/tests/fixtures/boxes.ts +++ b/ui/src/utils/tests/fixtures/boxes.ts @@ -42,7 +42,9 @@ interface FixtureData { } } -// Map containing each app's corresponding box fixture data +/** + * Map containing each application's corresponding box fixture data + */ export const boxFixtures: FixtureData = { '1010': { // Staking pool appId 1010 @@ -58,9 +60,8 @@ export const boxFixtures: FixtureData = { /** * Encodes staker information into a base64 string. - * * @param {StakedInfo[]} stakers - Array of staker information. - * @returns {string} - The base64 encoded string of stakers' data. + * @returns {string} The base64 encoded string of stakers' data. */ export function encodeStakersToBase64(stakers: StakedInfo[]): string { const bytesPerStaker = 64 diff --git a/ui/src/utils/tests/fixtures/methods.ts b/ui/src/utils/tests/fixtures/methods.ts index 65bcf506..f60abeb0 100644 --- a/ui/src/utils/tests/fixtures/methods.ts +++ b/ui/src/utils/tests/fixtures/methods.ts @@ -3,7 +3,9 @@ import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' // eslint-disable-next-line @typescript-eslint/no-explicit-any type FixtureFunction = (args: any) => any[] -// Map containing each ABI method's corresponding fixture function +/** + * Map containing each ABI method's mock response + */ export const methodFixtures: Record = { getPools: ({ validatorId }: { validatorId: number | bigint }) => { const pool1 = { diff --git a/ui/src/utils/tests/fixtures/nfd.ts b/ui/src/utils/tests/fixtures/nfd.ts new file mode 100644 index 00000000..4cb01218 --- /dev/null +++ b/ui/src/utils/tests/fixtures/nfd.ts @@ -0,0 +1,29 @@ +import { Nfd } from '@/interfaces/nfd' +import { ACCOUNT_3, ACCOUNT_4, ACCOUNT_5 } from '@/utils/tests/fixtures/accounts' + +export const MOCK_ROOT_NFD: Nfd = { + appID: 12345, + asaID: 67890, + avatarOutdated: false, + caAlgo: [ACCOUNT_3, ACCOUNT_4], + 'cache-control': 'max-age=3600', + category: 'standard', + currentAsOfBlock: 1000000, + depositAccount: ACCOUNT_3, + etag: 'abc123', + metaTags: ['tag1', 'tag2'], + name: 'example.algo', + nfdAccount: ACCOUNT_5, + owner: ACCOUNT_3, + properties: { + internal: { foo: 'bar' }, + userDefined: { foo: 'bar' }, + verified: { foo: 'bar' }, + }, + saleType: 'buyItNow', + state: 'owned', + tags: ['tag1', 'tag2'], + timeChanged: '2024-05-09T12:00:00Z', + timeCreated: '2024-05-08T12:00:00Z', + timePurchased: '2024-05-08T12:00:00Z', +} diff --git a/ui/src/utils/tests/fixtures/validators.ts b/ui/src/utils/tests/fixtures/validators.ts new file mode 100644 index 00000000..d86b289b --- /dev/null +++ b/ui/src/utils/tests/fixtures/validators.ts @@ -0,0 +1,153 @@ +import { ALGORAND_ZERO_ADDRESS_STRING } from '@/constants/accounts' +import { + Constraints, + NodeConfig, + PoolInfo, + Validator, + ValidatorConfig, + ValidatorState, +} from '@/interfaces/validator' +import { + ACCOUNT_1, + ACCOUNT_2, + ACCOUNT_3, + ACCOUNT_4, + ACCOUNT_5, +} from '@/utils/tests/fixtures/accounts' +import { createStaticArray } from '@/utils/tests/utils' + +export const MOCK_VALIDATOR_1_CONFIG: ValidatorConfig = { + id: 1, + owner: ACCOUNT_1, + manager: ACCOUNT_1, + nfdForInfo: 0, + entryGatingType: 0, + entryGatingAddress: ALGORAND_ZERO_ADDRESS_STRING, + entryGatingAssets: [0, 0, 0, 0], + gatingAssetMinBalance: 0n, + rewardTokenId: 0, + rewardPerPayout: 0n, + epochRoundLength: 1286, + percentToValidator: 5, + validatorCommissionAddress: ACCOUNT_1, + minEntryStake: 10n, + maxAlgoPerPool: 0n, + poolsPerNode: 1, + sunsettingOn: 0, + sunsettingTo: 0, +} + +export const MOCK_VALIDATOR_1_STATE: ValidatorState = { + numPools: 2, + totalStakers: 2, + totalAlgoStaked: 72000000000000n, + rewardTokenHeldBack: 0n, +} + +export const MOCK_VALIDATOR_1_POOLS: PoolInfo[] = [ + { + poolAppId: 1010, + totalStakers: 2, + totalAlgoStaked: 70000000000000n, + poolAddress: ACCOUNT_2, + algodVersion: '3.23.1 rel/stable [34171a94] : v0.8.0 [d346b13]', + }, + { + poolAppId: 1011, + totalStakers: 2, + totalAlgoStaked: 2000000000000n, + poolAddress: ACCOUNT_3, + algodVersion: '3.23.1 rel/stable [34171a94] : v0.8.0 [d346b13]', + }, +] + +export const MOCK_VALIDATOR_1_POOL_ASSIGNMENT: NodeConfig[] = createStaticArray( + [ + [70000000000000n, 0n, 0n], + [2000000000000n, 0n, 0n], + ], + [0n, 0n, 0n], + 8, +) + +export const MOCK_VALIDATOR_2_CONFIG: ValidatorConfig = { + id: 2, + owner: ACCOUNT_4, + manager: ACCOUNT_4, + nfdForInfo: 0, + entryGatingType: 0, + entryGatingAddress: ALGORAND_ZERO_ADDRESS_STRING, + entryGatingAssets: [0, 0, 0, 0], + gatingAssetMinBalance: 0n, + rewardTokenId: 0, + rewardPerPayout: 0n, + epochRoundLength: 1286, + percentToValidator: 5, + validatorCommissionAddress: ACCOUNT_4, + minEntryStake: 10n, + maxAlgoPerPool: 0n, + poolsPerNode: 1, + sunsettingOn: 0, + sunsettingTo: 0, +} + +export const MOCK_VALIDATOR_2_STATE: ValidatorState = { + numPools: 1, + totalStakers: 1, + totalAlgoStaked: 1000n, + rewardTokenHeldBack: 0n, +} + +export const MOCK_VALIDATOR_2_POOLS: PoolInfo[] = [ + { + poolAppId: 1020, + totalStakers: 1, + totalAlgoStaked: 1000n, + poolAddress: ACCOUNT_5, + algodVersion: '3.23.1 rel/stable [34171a94] : v0.8.0 [d346b13]', + }, +] + +export const MOCK_VALIDATOR_2_POOL_ASSIGNMENT: NodeConfig[] = createStaticArray( + [[1000n, 0n, 0n]], + [0n, 0n, 0n], + 8, +) + +export const MOCK_TOKEN_PAYOUT_RATIO = new Array(24).fill(0) + +const { id: validator1Id, ...validator1Config } = MOCK_VALIDATOR_1_CONFIG + +export const MOCK_VALIDATOR_1: Validator = { + id: validator1Id, + config: validator1Config, + state: MOCK_VALIDATOR_1_STATE, + pools: MOCK_VALIDATOR_1_POOLS, + tokenPayoutRatio: MOCK_TOKEN_PAYOUT_RATIO, + nodePoolAssignment: MOCK_VALIDATOR_1_POOL_ASSIGNMENT, +} + +const { id: validator2Id, ...validator2Config } = MOCK_VALIDATOR_2_CONFIG + +export const MOCK_VALIDATOR_2: Validator = { + id: validator1Id, + config: validator2Config, + state: MOCK_VALIDATOR_2_STATE, + pools: MOCK_VALIDATOR_2_POOLS, + tokenPayoutRatio: MOCK_TOKEN_PAYOUT_RATIO, + nodePoolAssignment: MOCK_VALIDATOR_2_POOL_ASSIGNMENT, +} + +export const MOCK_CONSTRAINTS: Constraints = { + payoutRoundsMin: 1, + payoutRoundsMax: 1000000, + commissionPctMin: 0, + commissionPctMax: 1000000, + minEntryStake: 1000000n, + maxAlgoPerPool: 70000000000000n, + maxAlgoPerValidator: 300000000000000n, + saturationThreshold: 200000000000000n, + maxNodes: 8, + maxPoolsPerNode: 3, + maxStakersPerPool: 200, +} diff --git a/ui/src/utils/tests/utils.ts b/ui/src/utils/tests/utils.ts index 7e89bd85..b2d24450 100644 --- a/ui/src/utils/tests/utils.ts +++ b/ui/src/utils/tests/utils.ts @@ -1,11 +1,10 @@ /** * Creates an array containing unique values followed by duplicates of a default value. - * - * @template T The type of the elements in the array. - * @param {T[]} values - An array of unique values to start the array. - * @param {T} defaultValue - The default value to fill the rest of the array. - * @param {number} length - The total desired length of the array. - * @returns {T[]} - An array of elements of type T. + * @template T The type of the elements in the array + * @param {T[]} values - An array of unique values to start the array + * @param {T} defaultValue - The default value to fill the rest of the array + * @param {number} length - The total desired length of the array + * @returns {T[]} An array of elements of type T */ export function createStaticArray(values: T[], defaultValue: T, length: number): T[] { const resultArray: T[] = [...values] @@ -23,8 +22,8 @@ export function createStaticArray(values: T[], defaultValue: T, length: numbe /** * Parses a box name string into encoding and value, decoding if necessary. - * @param {string} nameParam - The name parameter in the format 'encoding:value'. - * @returns {[string, string]} - A tuple containing the encoding and the (possibly decoded) value. + * @param {string} nameParam - The name parameter in the format 'encoding:value' + * @returns {[string, string]} A tuple containing the encoding and the (possibly decoded) value */ export function parseBoxName(nameParam: string): [string, string] { const [encoding, value] = nameParam.split(':', 2) diff --git a/ui/src/utils/ui.ts b/ui/src/utils/ui.ts index d32b0fe6..8d1d1d46 100644 --- a/ui/src/utils/ui.ts +++ b/ui/src/utils/ui.ts @@ -1,6 +1,19 @@ import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' -export function cn(...inputs: ClassValue[]) { +/** + * Uses clsx and tailwind-merge to construct className strings conditionally. + * @param {ClassValue[]} inputs - Any number of arguments, can be Object, Array, Boolean, or String + * @returns {string} The combined class names, merged without style conflicts + * @see {@link https://github.com/lukeed/clsx#usage} + * @see {@link https://github.com/dcastil/tailwind-merge} + * @example + * ```jsx + *

+ * Hello, world! + *

+ * ``` + */ +export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)) } diff --git a/ui/src/utils/validation.ts b/ui/src/utils/validation.ts index 7cf96304..a6796893 100644 --- a/ui/src/utils/validation.ts +++ b/ui/src/utils/validation.ts @@ -5,6 +5,11 @@ import { GatingType } from '@/constants/gating' import { Constraints } from '@/interfaces/validator' import { isValidName, isValidRoot } from '@/utils/nfd' +/** + * Validator schema definitions for form validation + * @see {@link https://zod.dev} + * @see {@link https://github.com/react-hook-form/resolvers#zod} + */ export const validatorSchemas = { owner: () => { return z @@ -222,6 +227,11 @@ export const validatorSchemas = { }, } +/** + * Validator schema refinement for entry gating + * @param {any} data - The form data + * @param {RefinementCtx} ctx - The refinement context + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export const entryGatingRefinement = (data: any, ctx: RefinementCtx) => { const { From cae5872f041feadc3265e208ce57d81e50d8b811 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 17:50:24 -0400 Subject: [PATCH 6/9] chore(ui): update non-major dependencies (#137) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 337 ++++++++++++++++++++++++++---------------------- ui/package.json | 36 +++--- 2 files changed, 203 insertions(+), 170 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dd6ffb0..89cdecde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,7 +107,7 @@ importers: version: 1.1.6(algosdk@2.7.0) '@hookform/resolvers': specifier: 3.3.4 - version: 3.3.4(react-hook-form@7.51.3) + version: 3.3.4(react-hook-form@7.51.4) '@perawallet/connect': specifier: 1.3.4 version: 1.3.4(algosdk@2.7.0) @@ -160,20 +160,20 @@ importers: specifier: 1.0.7 version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) '@tanstack/react-query': - specifier: 5.32.0 - version: 5.32.0(react@18.3.1) + specifier: 5.35.5 + version: 5.35.5(react@18.3.1) '@tanstack/react-query-devtools': - specifier: 5.32.0 - version: 5.32.0(@tanstack/react-query@5.32.0)(react@18.3.1) + specifier: 5.35.5 + version: 5.35.5(@tanstack/react-query@5.35.5)(react@18.3.1) '@tanstack/react-router': - specifier: 1.31.3 - version: 1.31.3(react-dom@18.3.1)(react@18.3.1) + specifier: 1.31.27 + version: 1.31.27(react-dom@18.3.1)(react@18.3.1) '@tanstack/react-table': specifier: 8.16.0 version: 8.16.0(react-dom@18.3.1)(react@18.3.1) '@tanstack/router-devtools': - specifier: 1.31.3 - version: 1.31.3(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1) + specifier: 1.31.27 + version: 1.31.27(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1) '@tremor/react': specifier: 3.16.2 version: 3.16.2(react-dom@18.3.1)(react@18.3.1)(tailwindcss@3.4.3) @@ -211,8 +211,8 @@ importers: specifier: 1.11.11 version: 1.11.11 lucide-react: - specifier: 0.376.0 - version: 0.376.0(react@18.3.1) + specifier: 0.378.0 + version: 0.378.0(react@18.3.1) lute-connect: specifier: 1.2.0 version: 1.2.0 @@ -232,11 +232,11 @@ importers: specifier: 18.3.1 version: 18.3.1(react@18.3.1) react-helmet-async: - specifier: 2.0.4 - version: 2.0.4(react-dom@18.3.1)(react@18.3.1) + specifier: 2.0.5 + version: 2.0.5(react@18.3.1) react-hook-form: - specifier: 7.51.3 - version: 7.51.3(react@18.3.1) + specifier: 7.51.4 + version: 7.51.4(react@18.3.1) sonner: specifier: 1.4.41 version: 1.4.41(react-dom@18.3.1)(react@18.3.1) @@ -254,29 +254,29 @@ importers: version: 10.0.0(react@18.3.1) vite-plugin-node-polyfills: specifier: 0.21.0 - version: 0.21.0(vite@5.2.10) + version: 0.21.0(vite@5.2.11) zod: - specifier: 3.23.5 - version: 3.23.5 + specifier: 3.23.8 + version: 3.23.8 devDependencies: '@playwright/test': - specifier: 1.43.1 - version: 1.43.1 + specifier: 1.44.0 + version: 1.44.0 '@tanstack/router-vite-plugin': - specifier: 1.30.0 - version: 1.30.0(vite@5.2.10) + specifier: 1.31.18 + version: 1.31.18(vite@5.2.11) '@testing-library/jest-dom': - specifier: 6.4.2 - version: 6.4.2(vitest@1.5.2) + specifier: 6.4.5 + version: 6.4.5(vitest@1.6.0) '@testing-library/react': - specifier: 15.0.5 - version: 15.0.5(react-dom@18.3.1)(react@18.3.1) + specifier: 15.0.7 + version: 15.0.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) '@types/big.js': specifier: 6.2.2 version: 6.2.2 '@types/node': - specifier: 20.12.7 - version: 20.12.7 + specifier: 20.12.11 + version: 20.12.11 '@types/react': specifier: 18.3.1 version: 18.3.1 @@ -291,10 +291,10 @@ importers: version: 7.8.0(eslint@8.57.0)(typescript@5.4.5) '@vitejs/plugin-react': specifier: 4.2.1 - version: 4.2.1(vite@5.2.10) + version: 4.2.1(vite@5.2.11) '@vitest/coverage-v8': - specifier: 1.5.2 - version: 1.5.2(vitest@1.5.2) + specifier: 1.6.0 + version: 1.6.0(vitest@1.6.0) algo-msgpack-with-bigint: specifier: 2.1.1 version: 2.1.1 @@ -314,11 +314,11 @@ importers: specifier: 24.0.0 version: 24.0.0 msw: - specifier: 2.2.14 - version: 2.2.14(typescript@5.4.5) + specifier: 2.3.0 + version: 2.3.0(typescript@5.4.5) playwright: - specifier: 1.43.1 - version: 1.43.1 + specifier: 1.44.0 + version: 1.44.0 postcss: specifier: 8.4.38 version: 8.4.38 @@ -327,16 +327,16 @@ importers: version: 3.4.3(ts-node@10.9.2) ts-node: specifier: 10.9.2 - version: 10.9.2(@types/node@20.12.7)(typescript@5.4.5) + version: 10.9.2(@types/node@20.12.11)(typescript@5.4.5) typescript: specifier: 5.4.5 version: 5.4.5 vite: - specifier: 5.2.10 - version: 5.2.10(@types/node@20.12.7) + specifier: 5.2.11 + version: 5.2.11(@types/node@20.12.11) vitest: - specifier: 1.5.2 - version: 1.5.2(@types/node@20.12.7)(jsdom@24.0.0) + specifier: 1.6.0 + version: 1.6.0(@types/node@20.12.11)(jsdom@24.0.0) packages: @@ -1184,12 +1184,12 @@ packages: tailwindcss: 3.4.3(ts-node@10.9.2) dev: false - /@hookform/resolvers@3.3.4(react-hook-form@7.51.3): + /@hookform/resolvers@3.3.4(react-hook-form@7.51.4): resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} peerDependencies: react-hook-form: ^7.0.0 dependencies: - react-hook-form: 7.51.3(react@18.3.1) + react-hook-form: 7.51.4(react@18.3.1) dev: false /@humanwhocodes/config-array@0.11.14: @@ -1227,7 +1227,7 @@ packages: '@inquirer/figures': 1.0.1 '@inquirer/type': 1.3.1 '@types/mute-stream': 0.0.4 - '@types/node': 20.12.7 + '@types/node': 20.12.11 '@types/wrap-ansi': 3.0.0 ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -1281,7 +1281,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1302,14 +1302,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.12.7) + jest-config: 29.7.0(@types/node@20.12.11) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1337,7 +1337,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 jest-mock: 29.7.0 dev: true @@ -1364,7 +1364,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.12.7 + '@types/node': 20.12.11 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1397,7 +1397,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.12.7 + '@types/node': 20.12.11 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -1485,7 +1485,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.12.7 + '@types/node': 20.12.11 '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true @@ -1611,8 +1611,8 @@ packages: engines: {node: '>=18'} dev: true - /@mswjs/interceptors@0.26.15: - resolution: {integrity: sha512-HM47Lu1YFmnYHKMBynFfjCp0U/yRskHj/8QEJW0CBEPOlw8Gkmjfll+S9b8M7V5CNDw2/ciRxjjnWeaCiblSIQ==} + /@mswjs/interceptors@0.29.1: + resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} engines: {node: '>=18'} dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -1833,6 +1833,14 @@ packages: playwright: 1.43.1 dev: true + /@playwright/test@1.44.0: + resolution: {integrity: sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.44.0 + dev: true + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -3004,47 +3012,47 @@ packages: '@stablelib/wipe': 1.0.1 dev: false - /@tanstack/history@1.28.9: - resolution: {integrity: sha512-WgTFJhHaZnGZPyt0H11xFhGGDj1MtA1mrUmdAjB/nhVpmsAYXsSB5O+hkF9N66u7MjbNb405wTb9diBsztvI5w==} + /@tanstack/history@1.31.16: + resolution: {integrity: sha512-rahAZXlR879P7dngDH7BZwGYiODA9D5Hqo6nUHn9GAURcqZU5IW0ZiT54dPtV5EPES7muZZmknReYueDHs7FFQ==} engines: {node: '>=12'} dev: false - /@tanstack/query-core@5.32.0: - resolution: {integrity: sha512-Z3flEgCat55DRXU5UMwYU1U+DgFZKA3iufyOKs+II7iRAo0uXkeU7PH5e6sOH1CGEag0IpKmZxlUFpCg6roSKw==} + /@tanstack/query-core@5.35.5: + resolution: {integrity: sha512-OMWvlEqG01RfGj+XZb/piDzPp0eZkkHWSDHt2LvE/fd1zWburP/xwm0ghk6Iv8cuPlP+ACFkZviKXK0OVt6lhg==} dev: false - /@tanstack/query-devtools@5.28.10: - resolution: {integrity: sha512-5UN629fKa5/1K/2Pd26gaU7epxRrYiT1gy+V+pW5K6hnf1DeUKK3pANSb2eHKlecjIKIhTwyF7k9XdyE2gREvQ==} + /@tanstack/query-devtools@5.32.1: + resolution: {integrity: sha512-7Xq57Ctopiy/4atpb0uNY5VRuCqRS/1fi/WBCKKX6jHMa6cCgDuV/AQuiwRXcKARbq2OkVAOrW2v4xK9nTbcCA==} dev: false - /@tanstack/react-query-devtools@5.32.0(@tanstack/react-query@5.32.0)(react@18.3.1): - resolution: {integrity: sha512-KWrzLoUjs9JtDSH3H2qbm5MjjykyAT8DkvP8tukw3gBG4ziu5WaWHciBjMsYSe1JB79AOxxGovzjW/Cd9+ofVw==} + /@tanstack/react-query-devtools@5.35.5(@tanstack/react-query@5.35.5)(react@18.3.1): + resolution: {integrity: sha512-4Xll14B9uhgEJ+uqZZ5tqZ7G1LDR7wGYgb+NOZHGn11TTABnlV8GWon7zDMqdaHeR5mjjuY1UFo9pbz39kuZKQ==} peerDependencies: - '@tanstack/react-query': ^5.32.0 + '@tanstack/react-query': ^5.35.5 react: ^18.0.0 dependencies: - '@tanstack/query-devtools': 5.28.10 - '@tanstack/react-query': 5.32.0(react@18.3.1) + '@tanstack/query-devtools': 5.32.1 + '@tanstack/react-query': 5.35.5(react@18.3.1) react: 18.3.1 dev: false - /@tanstack/react-query@5.32.0(react@18.3.1): - resolution: {integrity: sha512-+E3UudQtarnx9A6xhpgMZapyF+aJfNBGFMgI459FnduEZqT/9KhOWnMOneZahLRt52yzskSA0AuOyLkXHK0yBA==} + /@tanstack/react-query@5.35.5(react@18.3.1): + resolution: {integrity: sha512-sppX7L+PVn5GBV3In6zzj0zcKfnZRKhXbX1MfIfKo1OjIq2GMaopvAFOP0x1bRYTUk2ikrdYcQYOozX7PWkb8A==} peerDependencies: react: ^18.0.0 dependencies: - '@tanstack/query-core': 5.32.0 + '@tanstack/query-core': 5.35.5 react: 18.3.1 dev: false - /@tanstack/react-router@1.31.3(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-w6vjNTDOoDLvZ4D0f9UZHLPAIDlCDhlISKh+dybbFAM1Bin0eikfaEpMuelzMPaiy8YKziEPEpsqu2nfF9AQig==} + /@tanstack/react-router@1.31.27(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-OPHrHI8d/9luCUAibLSGvuDSW9GJVplG3WczRpb+3nYcRlKzHIrFKyalJ98xcwQ8Dt3TkhwKDQ7jUJySfkMh1g==} engines: {node: '>=12'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@tanstack/history': 1.28.9 + '@tanstack/history': 1.31.16 '@tanstack/react-store': 0.2.1(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -3099,14 +3107,14 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false - /@tanstack/router-devtools@1.31.3(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-0kq43q364YdpRyk5WI9U4XYM91statflFJiA45MHkpTL8WysBxUQoKFB35K34RqcQc+qiqoevazY0wl9OLtLgQ==} + /@tanstack/router-devtools@1.31.27(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-xbtlA65Z+VlaXfRmh3zzdkaY+2LHmVkq2Ln/F08qQ9VdLxRD/oclDTntpAMKEg9DCAbTQ4yYQHuHCQeVxr5PJQ==} engines: {node: '>=12'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@tanstack/react-router': 1.31.3(react-dom@18.3.1)(react@18.3.1) + '@tanstack/react-router': 1.31.27(react-dom@18.3.1)(react@18.3.1) clsx: 2.1.1 date-fns: 2.30.0 goober: 2.1.14(csstype@3.1.3) @@ -3121,11 +3129,11 @@ packages: engines: {node: '>=12'} dependencies: prettier: 3.2.5 - zod: 3.23.5 + zod: 3.23.8 dev: true - /@tanstack/router-vite-plugin@1.30.0(vite@5.2.10): - resolution: {integrity: sha512-UEp6f760tf5+lKKKKTRpOWSIneInlhsR0RTWY44oz/qhe2/VwB2I1qqlLJUz2960VuZSH3YMCvTP+wk1BFvCyQ==} + /@tanstack/router-vite-plugin@1.31.18(vite@5.2.11): + resolution: {integrity: sha512-rNbV4JckO1UPWikypsYlJqQWABTMEjANKMzQMGX9rmPomubXJqno6cgZXgjTG5bu2fXKALlCQrkNql+olQIzMg==} engines: {node: '>=12'} dependencies: '@babel/core': 7.24.5 @@ -3142,8 +3150,8 @@ packages: '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.5 - '@vitejs/plugin-react': 4.2.1(vite@5.2.10) - zod: 3.23.5 + '@vitejs/plugin-react': 4.2.1(vite@5.2.11) + zod: 3.23.8 transitivePeerDependencies: - supports-color - vite @@ -3180,8 +3188,8 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.4.2(vitest@1.5.2): - resolution: {integrity: sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==} + /@testing-library/jest-dom@6.4.5(vitest@1.6.0): + resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: '@jest/globals': '>= 28' @@ -3209,18 +3217,23 @@ packages: dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 1.5.2(@types/node@20.12.7)(jsdom@24.0.0) + vitest: 1.6.0(@types/node@20.12.11)(jsdom@24.0.0) dev: true - /@testing-library/react@15.0.5(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-ttodVWYA2i2w4hRa6krKrmS1vKxAEkwDz34y+CwbcrbZUxFzUYN3a5xZyFKo+K6LBseCRCUkwcjATpaNn/UsIA==} + /@testing-library/react@15.0.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==} engines: {node: '>=18'} peerDependencies: + '@types/react': ^18.0.0 react: ^18.0.0 react-dom: ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true dependencies: '@babel/runtime': 7.24.5 '@testing-library/dom': 10.1.0 + '@types/react': 18.3.1 '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -3420,7 +3433,7 @@ packages: /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.11 dev: true /@types/istanbul-lib-coverage@2.0.6: @@ -3450,13 +3463,19 @@ packages: /@types/mute-stream@0.0.4: resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} dependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.11 dev: true + /@types/node@20.12.11: + resolution: {integrity: sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==} + dependencies: + undici-types: 5.26.5 + /@types/node@20.12.7: resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} dependencies: undici-types: 5.26.5 + dev: true /@types/prompts@2.4.9: resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} @@ -3645,7 +3664,7 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react@4.2.1(vite@5.2.10): + /@vitejs/plugin-react@4.2.1(vite@5.2.11): resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -3656,15 +3675,15 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.5) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.2.10(@types/node@20.12.7) + vite: 5.2.11(@types/node@20.12.11) transitivePeerDependencies: - supports-color dev: true - /@vitest/coverage-v8@1.5.2(vitest@1.5.2): - resolution: {integrity: sha512-QJqxRnbCwNtbbegK9E93rBmhN3dbfG1bC/o52Bqr0zGCYhQzwgwvrJBG7Q8vw3zilX6Ryy6oa/mkZku2lLJx1Q==} + /@vitest/coverage-v8@1.6.0(vitest@1.6.0): + resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==} peerDependencies: - vitest: 1.5.2 + vitest: 1.6.0 dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -3679,43 +3698,43 @@ packages: std-env: 3.7.0 strip-literal: 2.1.0 test-exclude: 6.0.0 - vitest: 1.5.2(@types/node@20.12.7)(jsdom@24.0.0) + vitest: 1.6.0(@types/node@20.12.11)(jsdom@24.0.0) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@1.5.2: - resolution: {integrity: sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==} + /@vitest/expect@1.6.0: + resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} dependencies: - '@vitest/spy': 1.5.2 - '@vitest/utils': 1.5.2 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 chai: 4.4.1 dev: true - /@vitest/runner@1.5.2: - resolution: {integrity: sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==} + /@vitest/runner@1.6.0: + resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} dependencies: - '@vitest/utils': 1.5.2 + '@vitest/utils': 1.6.0 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.5.2: - resolution: {integrity: sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==} + /@vitest/snapshot@1.6.0: + resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} dependencies: magic-string: 0.30.10 pathe: 1.1.2 pretty-format: 29.7.0 dev: true - /@vitest/spy@1.5.2: - resolution: {integrity: sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==} + /@vitest/spy@1.6.0: + resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} dependencies: tinyspy: 2.2.1 dev: true - /@vitest/utils@1.5.2: - resolution: {integrity: sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==} + /@vitest/utils@1.6.0: + resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -5041,7 +5060,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.12.7) + jest-config: 29.7.0(@types/node@20.12.11) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -6809,7 +6828,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -6847,7 +6866,7 @@ packages: create-jest: 29.7.0 exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.12.7) + jest-config: 29.7.0(@types/node@20.12.11) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -6858,7 +6877,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.12.7): + /jest-config@29.7.0(@types/node@20.12.11): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -6873,7 +6892,7 @@ packages: '@babel/core': 7.24.5 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 babel-jest: 29.7.0(@babel/core@7.24.5) chalk: 4.1.2 ci-info: 3.9.0 @@ -6933,7 +6952,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -6949,7 +6968,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.12.7 + '@types/node': 20.12.11 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -7000,7 +7019,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 jest-util: 29.7.0 dev: true @@ -7055,7 +7074,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -7086,7 +7105,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 chalk: 4.1.2 cjs-module-lexer: 1.3.1 collect-v8-coverage: 1.0.2 @@ -7138,7 +7157,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -7163,7 +7182,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.11 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -7175,7 +7194,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.11 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -7477,8 +7496,8 @@ packages: yallist: 4.0.0 dev: true - /lucide-react@0.376.0(react@18.3.1): - resolution: {integrity: sha512-g91IX3ERD6yUR1TL2dsL4BkcGygpZz/EsqjAeL/kcRQV0EApIOr/9eBfKhYOVyQIcGGuotFGjF3xKLHMEz+b7g==} + /lucide-react@0.378.0(react@18.3.1): + resolution: {integrity: sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 dependencies: @@ -7656,8 +7675,8 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true - /msw@2.2.14(typescript@5.4.5): - resolution: {integrity: sha512-64i8rNCa1xzDK8ZYsTrVMli05D687jty8+Th+PU5VTbJ2/4P7fkQFVyDQ6ZFT5FrNR8z2BHhbY47fKNvfHrumA==} + /msw@2.3.0(typescript@5.4.5): + resolution: {integrity: sha512-cDr1q/QTMzaWhY8n9lpGhceY209k29UZtdTgJ3P8Bzne3TSMchX2EM/ldvn4ATLOktpCefCU2gcEgzHc31GTPw==} engines: {node: '>=18'} hasBin: true requiresBuild: true @@ -7671,7 +7690,7 @@ packages: '@bundled-es-modules/statuses': 1.0.1 '@inquirer/confirm': 3.1.6 '@mswjs/cookies': 1.1.0 - '@mswjs/interceptors': 0.26.15 + '@mswjs/interceptors': 0.29.1 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.5 @@ -8201,6 +8220,12 @@ packages: hasBin: true dev: true + /playwright-core@1.44.0: + resolution: {integrity: sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==} + engines: {node: '>=16'} + hasBin: true + dev: true + /playwright@1.43.1: resolution: {integrity: sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==} engines: {node: '>=16'} @@ -8211,6 +8236,16 @@ packages: fsevents: 2.3.2 dev: true + /playwright@1.44.0: + resolution: {integrity: sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.44.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -8259,7 +8294,7 @@ packages: dependencies: lilconfig: 3.1.1 postcss: 8.4.38 - ts-node: 10.9.2(@types/node@20.12.7)(typescript@5.4.5) + ts-node: 10.9.2(@types/node@20.12.11)(typescript@5.4.5) yaml: 2.4.2 /postcss-nested@6.0.1(postcss@8.4.38): @@ -8502,21 +8537,19 @@ packages: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-helmet-async@2.0.4(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-yxjQMWposw+akRfvpl5+8xejl4JtUlHnEBcji6u8/e6oc7ozT+P9PNTWMhCbz2y9tc5zPegw2BvKjQA+NwdEjQ==} + /react-helmet-async@2.0.5(react@18.3.1): + resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==} peerDependencies: react: ^16.6.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 dependencies: invariant: 2.2.4 react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) react-fast-compare: 3.2.2 shallowequal: 1.1.0 dev: false - /react-hook-form@7.51.3(react@18.3.1): - resolution: {integrity: sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==} + /react-hook-form@7.51.4(react@18.3.1): + resolution: {integrity: sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==} engines: {node: '>=12.22.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 @@ -9439,7 +9472,7 @@ packages: code-block-writer: 12.0.0 dev: true - /ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5): + /ts-node@10.9.2(@types/node@20.12.11)(typescript@5.4.5): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -9458,7 +9491,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.12.7 + '@types/node': 20.12.11 acorn: 8.11.3 acorn-walk: 8.3.2 arg: 4.1.3 @@ -9853,8 +9886,8 @@ packages: d3-timer: 3.0.1 dev: false - /vite-node@1.5.2(@types/node@20.12.7): - resolution: {integrity: sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==} + /vite-node@1.6.0(@types/node@20.12.11): + resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -9862,7 +9895,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.10(@types/node@20.12.7) + vite: 5.2.11(@types/node@20.12.11) transitivePeerDependencies: - '@types/node' - less @@ -9874,20 +9907,20 @@ packages: - terser dev: true - /vite-plugin-node-polyfills@0.21.0(vite@5.2.10): + /vite-plugin-node-polyfills@0.21.0(vite@5.2.11): resolution: {integrity: sha512-Sk4DiKnmxN8E0vhgEhzLudfJQfaT8k4/gJ25xvUPG54KjLJ6HAmDKbr4rzDD/QWEY+Lwg80KE85fGYBQihEPQA==} peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: '@rollup/plugin-inject': 5.0.5 node-stdlib-browser: 1.2.0 - vite: 5.2.10(@types/node@20.12.7) + vite: 5.2.11(@types/node@20.12.11) transitivePeerDependencies: - rollup dev: false - /vite@5.2.10(@types/node@20.12.7): - resolution: {integrity: sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==} + /vite@5.2.11(@types/node@20.12.11): + resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -9914,22 +9947,22 @@ packages: terser: optional: true dependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.11 esbuild: 0.20.2 postcss: 8.4.38 rollup: 4.17.2 optionalDependencies: fsevents: 2.3.3 - /vitest@1.5.2(@types/node@20.12.7)(jsdom@24.0.0): - resolution: {integrity: sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==} + /vitest@1.6.0(@types/node@20.12.11)(jsdom@24.0.0): + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.5.2 - '@vitest/ui': 1.5.2 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -9946,12 +9979,12 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.12.7 - '@vitest/expect': 1.5.2 - '@vitest/runner': 1.5.2 - '@vitest/snapshot': 1.5.2 - '@vitest/spy': 1.5.2 - '@vitest/utils': 1.5.2 + '@types/node': 20.12.11 + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 acorn-walk: 8.3.2 chai: 4.4.1 debug: 4.3.4 @@ -9965,8 +9998,8 @@ packages: strip-literal: 2.1.0 tinybench: 2.8.0 tinypool: 0.8.4 - vite: 5.2.10(@types/node@20.12.7) - vite-node: 1.5.2(@types/node@20.12.7) + vite: 5.2.11(@types/node@20.12.11) + vite-node: 1.6.0(@types/node@20.12.11) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -10242,5 +10275,5 @@ packages: engines: {node: '>=12.20'} dev: true - /zod@3.23.5: - resolution: {integrity: sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==} + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} diff --git a/ui/package.json b/ui/package.json index 5591982e..2e58c87a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,32 +11,32 @@ "node": "20" }, "devDependencies": { - "@playwright/test": "1.43.1", - "@tanstack/router-vite-plugin": "1.30.0", - "@testing-library/jest-dom": "6.4.2", - "@testing-library/react": "15.0.5", + "@playwright/test": "1.44.0", + "@tanstack/router-vite-plugin": "1.31.18", + "@testing-library/jest-dom": "6.4.5", + "@testing-library/react": "15.0.7", "@types/big.js": "6.2.2", - "@types/node": "20.12.7", + "@types/node": "20.12.11", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.8.0", "@typescript-eslint/parser": "7.8.0", "@vitejs/plugin-react": "4.2.1", - "@vitest/coverage-v8": "1.5.2", + "@vitest/coverage-v8": "1.6.0", "algo-msgpack-with-bigint": "2.1.1", "autoprefixer": "10.4.19", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-prettier": "5.1.3", "jsdom": "24.0.0", - "msw": "2.2.14", - "playwright": "1.43.1", + "msw": "2.3.0", + "playwright": "1.44.0", "postcss": "8.4.38", "tailwindcss": "3.4.3", "ts-node": "10.9.2", "typescript": "5.4.5", - "vite": "5.2.10", - "vitest": "1.5.2" + "vite": "5.2.11", + "vitest": "1.6.0" }, "dependencies": { "@algorandfoundation/algokit-utils": "6.0.2", @@ -59,11 +59,11 @@ "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-switch": "1.0.3", "@radix-ui/react-tooltip": "1.0.7", - "@tanstack/react-query": "5.32.0", - "@tanstack/react-query-devtools": "5.32.0", - "@tanstack/react-router": "1.31.3", + "@tanstack/react-query": "5.35.5", + "@tanstack/react-query-devtools": "5.35.5", + "@tanstack/react-router": "1.31.27", "@tanstack/react-table": "8.16.0", - "@tanstack/router-devtools": "1.31.3", + "@tanstack/router-devtools": "1.31.27", "@tremor/react": "3.16.2", "@txnlab/use-wallet-react": "3.0.0-beta.5", "@walletconnect/modal-sign-html": "2.6.2", @@ -76,22 +76,22 @@ "copy-to-clipboard": "3.3.3", "date-fns": "3.6.0", "dayjs": "1.11.11", - "lucide-react": "0.376.0", + "lucide-react": "0.378.0", "lute-connect": "1.2.0", "next-themes": "0.3.0", "notistack": "3.0.1", "react": "18.3.1", "react-day-picker": "8.10.1", "react-dom": "18.3.1", - "react-helmet-async": "2.0.4", - "react-hook-form": "7.51.3", + "react-helmet-async": "2.0.5", + "react-hook-form": "7.51.4", "sonner": "1.4.41", "tailwind-merge": "2.3.0", "tailwindcss-animate": "1.0.7", "tslib": "2.6.2", "use-debounce": "10.0.0", "vite-plugin-node-polyfills": "0.21.0", - "zod": "3.23.5" + "zod": "3.23.8" }, "scripts": { "dev": "vite", From 0b6bbedd976cd7949be7811c6409041d3112311f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 19:58:49 -0400 Subject: [PATCH 7/9] chore(bootstrap): update dependency @types/node to v20.12.11 (#126) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89cdecde..6afe331a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,7 +86,7 @@ importers: version: 18.2.4 '@types/node': specifier: ^20.12.2 - version: 20.12.7 + version: 20.12.11 '@types/prompts': specifier: ^2.4.9 version: 2.4.9 @@ -3471,16 +3471,10 @@ packages: dependencies: undici-types: 5.26.5 - /@types/node@20.12.7: - resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} - dependencies: - undici-types: 5.26.5 - dev: true - /@types/prompts@2.4.9: resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} dependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.11 kleur: 3.0.3 dev: true From 0b6475b1b33ac5247d1e60c15ce1eff18163e4aa Mon Sep 17 00:00:00 2001 From: Patrick Bennett Date: Tue, 14 May 2024 13:59:45 -0400 Subject: [PATCH 8/9] fix(nodemgr): if reward token is specified, then the token asset needs added to foreign assets in epochUpdate (and pool 1 needs in foreign apps if not updating pool 1) --- nodemgr/internal/lib/reti/stakingpool.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/nodemgr/internal/lib/reti/stakingpool.go b/nodemgr/internal/lib/reti/stakingpool.go index 81482408..185b9a66 100644 --- a/nodemgr/internal/lib/reti/stakingpool.go +++ b/nodemgr/internal/lib/reti/stakingpool.go @@ -173,10 +173,18 @@ func (r *Reti) EpochBalanceUpdate(poolID int, poolAppID uint64, caller types.Add newParams.Fee = 0 extraApps := []uint64{} + extraAssets := []uint64{} if r.info.Config.NFDForInfo != 0 { extraApps = append(extraApps, r.info.Config.NFDForInfo) } + if r.info.Config.RewardTokenId != 0 { + extraAssets = append(extraAssets, r.info.Config.RewardTokenId) + if poolID != 1 { + // If not pool 1 then we need to add reference for pool 1, so it can be called to update the pool token payout ratio + extraApps = append(extraApps, r.info.Pools[0].PoolAppId) + } + } // we need to stack up references in these two gas methods for resource pooling err = atc.AddMethodCall(transaction.AddMethodCallParams{ @@ -201,9 +209,10 @@ func (r *Reti) EpochBalanceUpdate(poolID int, poolAppID uint64, caller types.Add return atc, err } err = atc.AddMethodCall(transaction.AddMethodCallParams{ - AppID: poolAppID, - Method: gasMethod, - ForeignApps: extraApps, + AppID: poolAppID, + Method: gasMethod, + ForeignAssets: extraAssets, + ForeignApps: extraApps, ForeignAccounts: []string{ info.Config.ValidatorCommissionAddress, r.info.Config.Manager, From e559394c9e5c5770b99aa576f25b1e6fcbe2471d Mon Sep 17 00:00:00 2001 From: Patrick Bennett Date: Tue, 14 May 2024 14:01:28 -0400 Subject: [PATCH 9/9] chore: update to v0.8.4 --- contracts/bootstrap/package.json | 2 +- contracts/package.json | 2 +- ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/bootstrap/package.json b/contracts/bootstrap/package.json index 852f1fef..5751cb2f 100644 --- a/contracts/bootstrap/package.json +++ b/contracts/bootstrap/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap", - "version": "0.8.3", + "version": "0.8.4", "description": "", "main": "index.ts", "scripts": { diff --git a/contracts/package.json b/contracts/package.json index 09ed080a..fc5b64a8 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "reti-contracts", - "version": "0.8.3", + "version": "0.8.4", "license": "MIT", "scripts": { "generate-client": "algokit generate client contracts/artifacts/ --language typescript --output contracts/clients/{contract_name}Client.ts && ./update_contract_artifacts.sh``", diff --git a/ui/package.json b/ui/package.json index 2e58c87a..61cdf9be 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "reti-ui", - "version": "0.8.3", + "version": "0.8.4", "author": { "name": "Doug Richar", "email": "drichar@gmail.com"