diff --git a/README.md b/README.md
index cba7f0c..c8f29a6 100644
--- a/README.md
+++ b/README.md
@@ -70,31 +70,43 @@ For more information on Jettons compatibility, see [Jettons compatibility](/jett
## Deployment
### Configurable parameters
-| ENV variable | Description |
-|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `LITESERVER` | IP and port of lite server, example: `185.86.76.183:5815` |
-| `LITESERVER_KEY` | public key of lite server `5v2dHtSclsGsZVbNVwTj4hQDso5xvQjzL/yPEHJevHk=`.
Be careful with base64 encoding and ENV var. Use '' |
-| `SEED` | seed phrase for main hot wallet. 24 words compatible with standard TON wallets |
-| `DB_URI` | URI for DB connection, example:
`postgresql://db_user:db_password@localhost:5432/payment_processor` |
-| `API_HOST` | host for REST API, example `localhost:8081`, default `0.0.0.0:8081` |
-| `API_TOKEN` | Bearer token for REST API, example `123` |
-| `IS_TESTNET` | `true` if service works in TESTNET, `false` - for MAINNET. Default: `true`. |
-| `JETTONS` | list of Jettons, processed by service in format:
`JETTON_SYMBOL_1:MASTER_CONTRACT_ADDR_1:hot_wallet_max_balance:min_withdrawal_amount, JETTON_SYMBOL_2:MASTER_CONTRACT_ADDR_2:hot_wallet_max_balance:min_withdrawal_amount`,
example: `TGR:kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0:1000000:100000` |
-| `TON_CUTOFFS` | cutoffs in nanoTONs in format:
`hot_wallet_min_balance:hot_wallet_max_balance:min_withdrawal_amount`,
example `1000000000:100000000000:1000000000` |
-| `COLD_WALLET` | cold-wallet address, example `kQCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseH3XZ_YiH9Y1ufw` |
-| `DEPOSIT_SIDE_BALANCE` | `true` - service calculates total income for user by deposit incoming, `false` - by hot wallet incoming. Default: `true`. |
-| `QUEUE_ENABLED` | `true` - service sends incoming notifications to queue, `false` - sending disabled. Default: `false`. |
-| `QUEUE_URI` | URI for queue client connection, example `amqp://guest:guest@payment_rabbitmq:5672/` |
-| `QUEUE_NAME` | name of exchange |
-| `WEBHOOK_ENDPOINT` | endpoint to send webhooks, example: `http://hostname:3333/webhook`. If the value is not set, then webhooks are not sent. |
-| `WEBHOOK_TOKEN` | Bearer token for webhook request. If not set then not used. |
-| `ALLOWABLE_LAG` | allowable time lag between service time and last block time in seconds, default: 15 |
+| ENV variable | Description |
+|------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `LITESERVER` | IP and port of lite server, example: `185.86.76.183:5815` |
+| `LITESERVER_KEY` | public key of lite server `5v2dHtSclsGsZVbNVwTj4hQDso5xvQjzL/yPEHJevHk=`.
Be careful with base64 encoding and ENV var. Use '' |
+| `SEED` | seed phrase for main hot wallet. 24 words compatible with standard TON wallets |
+| `DB_URI` | URI for DB connection, example:
`postgresql://db_user:db_password@localhost:5432/payment_processor` |
+| `API_HOST` | host for REST API, example `localhost:8081`, default `0.0.0.0:8081` |
+| `API_TOKEN` | Bearer token for REST API, example `123` |
+| `IS_TESTNET` | `true` if service works in TESTNET, `false` - for MAINNET. Default: `true`. |
+| `JETTONS` | list of Jettons, processed by service in format:
`JETTON_SYMBOL_1:MASTER_CONTRACT_ADDR_1:hot_wallet_max_balance:min_withdrawal_amount:hot_wallet_residual_balance, JETTON_SYMBOL_2:MASTER_CONTRACT_ADDR_2:hot_wallet_max_balance:min_withdrawal_amount:hot_wallet_residual_balance`,
example: `TGR:kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0:1000000:100000` |
+| `TON_CUTOFFS` | cutoffs in nanoTONs in format:
`hot_wallet_min_balance:hot_wallet_max_balance:min_withdrawal_amount:hot_wallet_residual_balance`,
example `1000000000:100000000000:1000000000:95000000000` |
+| `COLD_WALLET` | cold-wallet address, example `kQCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseH3XZ_YiH9Y1ufw`. If cold wallet is not active - use non-bounceable address (use https://ton.org/address for convert) |
+| `DEPOSIT_SIDE_BALANCE` | `true` - service calculates total income for user by deposit incoming, `false` - by hot wallet incoming. Default: `true`. |
+| `QUEUE_ENABLED` | `true` - service sends incoming notifications to queue, `false` - sending disabled. Default: `false`. |
+| `QUEUE_URI` | URI for queue client connection, example `amqp://guest:guest@payment_rabbitmq:5672/` |
+| `QUEUE_NAME` | name of exchange |
+| `WEBHOOK_ENDPOINT` | endpoint to send webhooks, example: `http://hostname:3333/webhook`. If the value is not set, then webhooks are not sent. |
+| `WEBHOOK_TOKEN` | Bearer token for webhook request. If not set then not used. |
+| `ALLOWABLE_LAG` | allowable time lag between service time and last block time in seconds, default: 15 |
**! Be careful with `IS_TESTNET` variable.** This does not guarantee that a testnet node is being used. It is only for address checking purposes.
There are also internal service settings (fees and timeouts) that are specified in the source code in the [Config](/config/config.go) package.
Calibration parameters recommendations in [Technical notes](/technical_notes.md).
+#### `hot_wallet_residual_balance` and `hot_wallet_max_balance`
+
+In order to avoid triggering a withdrawal to a cold wallet with each receipt of funds, a hysteresis is introduced.
+`hot_wallet_max_balance` - this is the amount at which the withdrawal from the hot wallet to the cold one will be triggered
+`hot_wallet_residual_balance` is the amount that will remain on the hot wallet after the withdrawal
+
+`hot_wallet_max_balance` must be greater than `hot_wallet_residual_balance`
+
+If the `hot_wallet_residual_balance` is not set, then it is calculated using the formula:
+`hot_wallet_residual_balance` = `hot_wallet_max_balance` * `hysteresis`, where hysteresis is a hardcoded value
+(at the time of writing this is 0.95)
+
### Service deploy
**Do not use same `.env` file for `payment-processor` and other services!**
@@ -136,7 +148,9 @@ Message format when `DEPOSIT_SIDE_BALANCE` == true:
"time": 12345678,
"amount":"100",
"source_address":"0QAOp2OZwWdkF5HhJ0WVDspgh6HhpmHyQ3cBuBmfJ4q_AIVe",
- "comment":"hello"
+ "comment":"hello",
+ "tx_hash": "f9b9e7efd3a38da318a894576499f0b6af5ca2da97ccd15c5f1d291a808a0ebf",
+ "user_id": "123"
}
```
@@ -146,7 +160,9 @@ from the deposit):
{
"deposit_address":"0QCdsj-u39qVlfYdpPKuAY0hTe5VIsiJcpB5Rx4tOUOyBFhL",
"time": 12345678,
- "amount":"200"
+ "amount":"200",
+ "tx_hash": "f9b9e7efd3a38da318a894576499f0b6af5ca2da97ccd15c5f1d291a808a0ebf",
+ "user_id": "123"
}
```
diff --git a/blockchain/blockchain_test.go b/blockchain/blockchain_test.go
index a5c4886..709aee6 100644
--- a/blockchain/blockchain_test.go
+++ b/blockchain/blockchain_test.go
@@ -215,7 +215,7 @@ func Test_GetAccountCurrentState(t *testing.T) {
func Test_DeployTonWallet(t *testing.T) {
c := connect(t)
seed := getSeed()
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*120)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*200)
defer cancel()
amount := tlb.FromNanoTONU(100_000_000)
mainWallet, _, _, err := c.GenerateDefaultWallet(seed, true)
@@ -229,7 +229,7 @@ func Test_DeployTonWallet(t *testing.T) {
if b.Cmp(amount.NanoTON()) != 1 || st != tlb.AccountStatusActive {
t.Fatal("wallet not active")
}
- newWallet, err := mainWallet.GetSubwallet(3567745334)
+ newWallet, err := mainWallet.GetSubwallet(rand.Uint32())
if err != nil {
t.Fatal("gen new wallet err: ", err)
}
diff --git a/cmd/processor/main.go b/cmd/processor/main.go
index 27c880e..265d967 100644
--- a/cmd/processor/main.go
+++ b/cmd/processor/main.go
@@ -54,7 +54,7 @@ func main() {
wallets, err := core.InitWallets(ctx, dbClient, bcClient, config.Config.Seed, config.Config.Jettons)
if err != nil {
- log.Fatalf("Hot wallets initialization error: %v", err)
+ log.Fatalf("Wallets initialization error: %v", err)
}
var notificators []core.Notificator
diff --git a/config/config.go b/config/config.go
index 4cfb5d3..0997ada 100644
--- a/config/config.go
+++ b/config/config.go
@@ -16,6 +16,8 @@ var (
JettonTransferTonAmount = tlb.FromNanoTONU(100_000_000)
JettonForwardAmount = tlb.FromNanoTONU(20_000_000) // must be < JettonTransferTonAmount
+ DefaultHotWalletHysteresis = decimal.NewFromFloat(0.95) // `hot_wallet_residual_balance` = `hot_wallet_max_balance` * `hysteresis`
+
ExternalMessageLifetime = 50 * time.Second
ExternalWithdrawalPeriod = 80 * time.Second // must be ExternalWithdrawalPeriod > ExternalMessageLifetime and some time for balance update
@@ -62,12 +64,14 @@ type Jetton struct {
Master *address.Address
WithdrawalCutoff *big.Int
HotWalletMaxCutoff *big.Int
+ HotWalletResidual *big.Int
}
type Cutoffs struct {
- HotWalletMin *big.Int
- HotWalletMax *big.Int
- Withdrawal *big.Int
+ HotWalletMin *big.Int
+ HotWalletMax *big.Int
+ Withdrawal *big.Int
+ HotWalletResidual *big.Int
}
func GetConfig() {
@@ -119,7 +123,7 @@ func parseJettonString(s string) map[string]Jetton {
jettons := strings.Split(s, ",")
for _, j := range jettons {
data := strings.Split(j, ":")
- if len(data) != 4 {
+ if len(data) != 4 && len(data) != 5 {
log.Fatalf("invalid jetton data")
}
cur := data[0]
@@ -135,10 +139,20 @@ func parseJettonString(s string) map[string]Jetton {
if err != nil {
log.Fatalf("invalid %v jetton withdrawal cutoff: %v", data[0], err)
}
+
+ residual := maxCutoff.Mul(DefaultHotWalletHysteresis)
+ if len(data) == 5 {
+ residual, err = decimal.NewFromString(data[4])
+ if err != nil {
+ log.Fatalf("invalid hot_wallet_residual_balance parameter: %v", err)
+ }
+ }
+
res[cur] = Jetton{
Master: addr,
WithdrawalCutoff: withdrawalCutoff.BigInt(),
HotWalletMaxCutoff: maxCutoff.BigInt(),
+ HotWalletResidual: residual.BigInt(),
}
}
return res
@@ -146,8 +160,8 @@ func parseJettonString(s string) map[string]Jetton {
func parseTonString(s string) Cutoffs {
data := strings.Split(s, ":")
- if len(data) != 3 {
- log.Fatalf("invalid jetton data")
+ if len(data) != 3 && len(data) != 4 {
+ log.Fatalf("invalid TON cuttofs")
}
hotWalletMin, err := decimal.NewFromString(data[0])
if err != nil {
@@ -164,9 +178,19 @@ func parseTonString(s string) Cutoffs {
if hotWalletMin.Cmp(hotWalletMax) == 1 {
log.Fatalf("TON hot wallet max cutoff must be greater than TON hot wallet min cutoff")
}
+
+ residual := hotWalletMax.Mul(DefaultHotWalletHysteresis)
+ if len(data) == 4 {
+ residual, err = decimal.NewFromString(data[3])
+ if err != nil {
+ log.Fatalf("invalid hot_wallet_residual_balance parameter: %v", err)
+ }
+ }
+
return Cutoffs{
- HotWalletMin: hotWalletMin.BigInt(),
- HotWalletMax: hotWalletMax.BigInt(),
- Withdrawal: withdrawal.BigInt(),
+ HotWalletMin: hotWalletMin.BigInt(),
+ HotWalletMax: hotWalletMax.BigInt(),
+ Withdrawal: withdrawal.BigInt(),
+ HotWalletResidual: residual.BigInt(),
}
}
diff --git a/core/block_scanner.go b/core/block_scanner.go
index 66f782a..172c9d5 100644
--- a/core/block_scanner.go
+++ b/core/block_scanner.go
@@ -58,6 +58,8 @@ type incomeNotification struct {
Amount string `json:"amount"`
Source string `json:"source_address,omitempty"`
Comment string `json:"comment,omitempty"`
+ UserID string `json:"user_id"`
+ TxHash string `json:"tx_hash"`
}
func NewBlockScanner(
@@ -133,14 +135,14 @@ func (s *BlockScanner) pushNotifications(e BlockEvents) error {
if config.Config.IsDepositSideCalculation {
for _, ei := range e.ExternalIncomes {
- err := s.pushNotification(ei.To, ei.Amount, ei.Utime, ei.From, ei.FromWorkchain, ei.Comment)
+ err := s.pushNotification(ei.To, ei.Amount, ei.Utime, ei.From, ei.FromWorkchain, ei.Comment, ei.TxHash)
if err != nil {
return err
}
}
} else {
for _, ii := range e.InternalIncomes {
- err := s.pushNotification(ii.From, ii.Amount, ii.Utime, nil, nil, "")
+ err := s.pushNotification(ii.From, ii.Amount, ii.Utime, nil, nil, "", ii.TxHash)
if err != nil {
return err
}
@@ -156,16 +158,23 @@ func (s *BlockScanner) pushNotification(
from []byte,
fromWorkchain *int32,
comment string,
+ txHash []byte,
) error {
owner := s.db.GetOwner(addr)
if owner != nil {
addr = *owner
}
+ userID, ok := s.db.GetUserID(addr)
+ if !ok {
+ return fmt.Errorf("not found UserID for deposit %s", addr.ToUserFormat())
+ }
notification := incomeNotification{
Deposit: addr.ToUserFormat(),
Amount: amount.String(),
Timestamp: int64(timestamp),
Comment: comment,
+ UserID: userID,
+ TxHash: fmt.Sprintf("%x", txHash),
}
if len(from) == 32 && fromWorkchain != nil {
// supports only std address
@@ -433,6 +442,7 @@ func convertUnknownJettonTxs(txs []*tlb.Transaction, addr Address, amount *big.I
Lt: tx.LT,
To: addr,
Amount: ZeroCoins(),
+ TxHash: tx.Hash,
})
}
@@ -442,6 +452,7 @@ func convertUnknownJettonTxs(txs []*tlb.Transaction, addr Address, amount *big.I
Lt: txs[0].LT,
To: addr,
Amount: NewCoins(amount),
+ TxHash: txs[0].Hash,
})
}
return incomes, nil
@@ -746,6 +757,7 @@ func (s *BlockScanner) processTonHotWalletInternalInMsg(tx *tlb.Transaction) (Ev
Amount: NewCoins(inMsg.Amount.NanoTON()),
Memo: inMsg.Comment(),
IsFailed: false,
+ TxHash: tx.Hash,
}
success, err := checkTxForSuccess(tx)
if err != nil {
@@ -777,6 +789,7 @@ func (s *BlockScanner) processTonHotWalletInternalInMsg(tx *tlb.Transaction) (Ev
Amount: income.Amount,
Memo: income.Comment,
IsFailed: false,
+ TxHash: tx.Hash,
})
}
}
@@ -854,6 +867,7 @@ func (s *BlockScanner) processTonDepositWalletInternalInMsg(tx *tlb.Transaction)
To: dstAddr,
Amount: NewCoins(inMsg.Amount.NanoTON()),
Comment: inMsg.Comment(),
+ TxHash: tx.Hash,
})
}
return events, nil
@@ -927,6 +941,7 @@ func (s *BlockScanner) processJettonDepositOutMsgs(tx *tlb.Transaction) (Events,
To: srcAddr,
Amount: notify.Amount,
Comment: notify.Comment,
+ TxHash: tx.Hash,
})
knownIncomeAmount.Add(knownIncomeAmount, notify.Amount.BigInt())
}
diff --git a/core/models.go b/core/models.go
index ebd3a5d..8572042 100644
--- a/core/models.go
+++ b/core/models.go
@@ -131,8 +131,9 @@ func AddressMustFromTonutilsAddress(addr *address.Address) Address {
}
type AddressInfo struct {
- Type WalletType
- Owner *Address
+ Type WalletType
+ Owner *Address
+ UserID string
}
type JettonWallet struct {
@@ -222,6 +223,7 @@ type InternalIncome struct {
Amount Coins
Memo string
IsFailed bool
+ TxHash []byte
}
type ExternalIncome struct {
@@ -232,6 +234,7 @@ type ExternalIncome struct {
To Address
Amount Coins
Comment string
+ TxHash []byte
}
type Events struct {
@@ -297,6 +300,7 @@ type storage interface {
SaveJettonWallet(ctx context.Context, ownerAddress Address, walletData WalletData, notSaveOwner bool) error
GetWalletType(address Address) (WalletType, bool)
GetOwner(address Address) *Address
+ GetUserID(address Address) (string, bool)
GetWalletTypeByTonutilsAddress(address *address.Address) (WalletType, bool)
SaveParsedBlockData(ctx context.Context, events BlockEvents) error
GetTonInternalWithdrawalTasks(ctx context.Context, limit int) ([]InternalWithdrawalTask, error)
diff --git a/core/wallets.go b/core/wallets.go
index 2d97825..fb010ad 100644
--- a/core/wallets.go
+++ b/core/wallets.go
@@ -33,6 +33,18 @@ func InitWallets(
seed string,
jettons map[string]config.Jetton,
) (Wallets, error) {
+
+ if config.Config.ColdWallet != nil && config.Config.ColdWallet.IsBounceable() {
+ _, status, err := bc.GetAccountCurrentState(ctx, config.Config.ColdWallet)
+ if err != nil {
+ return Wallets{}, err
+ }
+ log.Infof("Cold wallet status: %s", status)
+ if status != tlb.AccountStatusActive {
+ return Wallets{}, fmt.Errorf("cold wallet address must be non-bounceable for not active wallet")
+ }
+ }
+
tonHotWallet, shard, subwalletId, err := initTonHotWallet(ctx, db, bc, seed)
if err != nil {
return Wallets{}, err
diff --git a/core/withdrawal_processor.go b/core/withdrawal_processor.go
index 346aca1..fff43be 100644
--- a/core/withdrawal_processor.go
+++ b/core/withdrawal_processor.go
@@ -631,7 +631,7 @@ func (p *WithdrawalsProcessor) makeColdWalletWithdrawals(ctx context.Context) er
if err != nil {
return err
}
- jettonAmount.Sub(jettonBalance, config.Config.Jettons[cur].HotWalletMaxCutoff)
+ jettonAmount.Sub(jettonBalance, config.Config.Jettons[cur].HotWalletResidual)
tonBalance.Sub(tonBalance, config.JettonTransferTonAmount.NanoTON())
req := WithdrawalRequest{
Currency: jw.Currency,
@@ -665,7 +665,7 @@ func (p *WithdrawalsProcessor) makeColdWalletWithdrawals(ctx context.Context) er
if err != nil {
return err
}
- tonAmount.Sub(tonBalance, config.Config.Ton.HotWalletMax)
+ tonAmount.Sub(tonBalance, config.Config.Ton.HotWalletResidual)
req := WithdrawalRequest{
Currency: TonSymbol,
Amount: NewCoins(tonAmount),
diff --git a/db/db.go b/db/db.go
index 3935eec..21427b8 100644
--- a/db/db.go
+++ b/db/db.go
@@ -57,6 +57,11 @@ func (c *Connection) GetWalletType(address core.Address) (core.WalletType, bool)
return info.Type, ok
}
+func (c *Connection) GetUserID(address core.Address) (string, bool) {
+ info, ok := c.addressBook.get(address)
+ return info.UserID, ok
+}
+
// GetOwner returns owner for jetton deposit from address book and nil for other types
func (c *Connection) GetOwner(address core.Address) *core.Address {
info, ok := c.addressBook.get(address)
@@ -103,7 +108,7 @@ func (c *Connection) SaveTonWallet(ctx context.Context, walletData core.WalletDa
if err != nil {
return err
}
- c.addressBook.put(walletData.Address, core.AddressInfo{Type: walletData.Type, Owner: nil})
+ c.addressBook.put(walletData.Address, core.AddressInfo{Type: walletData.Type, Owner: nil, UserID: walletData.UserID})
return nil
}
@@ -186,7 +191,7 @@ func (c *Connection) SaveJettonWallet(
// cold wallets excluded from address book
c.addressBook.put(ownerAddress, core.AddressInfo{Type: core.JettonOwner, Owner: nil})
}
- c.addressBook.put(walletData.Address, core.AddressInfo{Type: walletData.Type, Owner: &ownerAddress})
+ c.addressBook.put(walletData.Address, core.AddressInfo{Type: walletData.Type, Owner: &ownerAddress, UserID: walletData.UserID})
return nil
}
@@ -258,12 +263,13 @@ func (c *Connection) GetJettonOwnersAddresses(
func (c *Connection) LoadAddressBook(ctx context.Context) error {
res := make(map[core.Address]core.AddressInfo)
var (
- addr core.Address
- t core.WalletType
+ addr core.Address
+ t core.WalletType
+ userID string
)
rows, err := c.client.Query(ctx, `
- SELECT address, type
+ SELECT address, type, user_id
FROM payments.ton_wallets
`)
if err != nil {
@@ -271,15 +277,15 @@ func (c *Connection) LoadAddressBook(ctx context.Context) error {
}
defer rows.Close()
for rows.Next() {
- err = rows.Scan(&addr, &t)
+ err = rows.Scan(&addr, &t, &userID)
if err != nil {
return err
}
- res[addr] = core.AddressInfo{Type: t, Owner: nil}
+ res[addr] = core.AddressInfo{Type: t, Owner: nil, UserID: userID}
}
rows, err = c.client.Query(ctx, `
- SELECT jw.address, jw.type, tw.address
+ SELECT jw.address, jw.type, tw.address, jw.user_id
FROM payments.jetton_wallets jw
LEFT JOIN payments.ton_wallets tw ON jw.subwallet_id = tw.subwallet_id
`)
@@ -289,11 +295,11 @@ func (c *Connection) LoadAddressBook(ctx context.Context) error {
defer rows.Close()
for rows.Next() {
var owner core.Address
- err = rows.Scan(&addr, &t, &owner)
+ err = rows.Scan(&addr, &t, &owner, &userID)
if err != nil {
return err
}
- res[addr] = core.AddressInfo{Type: t, Owner: &owner}
+ res[addr] = core.AddressInfo{Type: t, Owner: &owner, UserID: userID}
}
c.addressBook.addresses = res
diff --git a/manual_testing_plan.md b/manual_testing_plan.md
index 3b95a7f..e6c0c0b 100644
--- a/manual_testing_plan.md
+++ b/manual_testing_plan.md
@@ -1,4 +1,4 @@
-## Manual testing plan for v0.2.0
+## Manual testing plan for v0.4.0
Template:
-[x] Checked
- TEST : test description
@@ -57,6 +57,11 @@ Template:
- RESULT : Service must stop. Must be address duplication error message in audit log.
- COMMENT :
+10. -[x] Checked
+- TEST : Start service with uninitialized cold wallet and bounceable address for cold wallet.
+- RESULT : Service must stop. Must be invalid address format error message in log.
+- COMMENT :
+
### API
1. -[x] Checked
@@ -234,7 +239,7 @@ Template:
should always be displayed.
- COMMENT :
-26. -[ ] Checked
+26. -[x] Checked
- TEST : Replenish the TON deposit from the masterchain wallet and check it by
`/v1/history{?user_id,currency,limit,offset}` method.
- RESULT : The sender's address must be displayed correctly in the history.
@@ -249,7 +254,7 @@ Template:
28. -[x] Checked
- TEST : Replenish the Jetton deposit with zero forward amount and check it by
- `/v1/history{?user_id,currency,limit,offset}` method.
+ `/v1/history{?user_id,currency,limit,offset}` method.
- RESULT : The sender's address must be not presented in the history.
- COMMENT :
@@ -257,9 +262,10 @@ Template:
1. -[x] Checked
- TEST : Replenish the deposit with TONs and Jettons so that as a result the amount on the hot wallet is greater
- than hot_wallet_max_balance. Check withdrawals in DB
-- RESULT : You must find new withdrawal in `withdrawal_requests` table with `is_internal=true`. And final status
- must correlate with explorer.
+ than `hot_wallet_max_balance` when cold wallet is not active and cold wallet address in non-bounceable format.
+ Check withdrawals in DB
+- RESULT : You must find new withdrawal in `withdrawal_requests` table with `is_internal=true` and `bounceable=false`.
+ And final status must correlate with explorer.
- COMMENT :
2. -[ ] Checked
@@ -268,6 +274,14 @@ Template:
- RESULT : There should be no missing blocks in the DB.
- COMMENT :
+3. -[x] Checked
+- TEST : Replenish the deposit with TONs and Jettons so that as a result the amount on the hot wallet is greater
+ than `hot_wallet_max_balance`. Try with and without `hot_wallet_residual_balance` parameter. Check withdrawals in DB
+- RESULT : You must find new withdrawal in `withdrawal_requests` table with `is_internal=true`. And final status
+ must correlate with explorer. Withdrawal amount must correlate with hysteresis formula
+ (and `hot_wallet_residual_balance` parameter).
+- COMMENT :
+
### Deploy
1. -[x] Checked
@@ -319,7 +333,7 @@ Template:
### Stability test
-1. -[ ] Checked
+1. -[x] Checked
- TEST : Start `payment-test` service using technical_notes.md instructions
with `CIRCULATION=true` env variable for long time (with enough amount of test TONs on wallet).
Periodically check availability and functionality of service by Grafana dashboard and docker logs.
diff --git a/threat_model.md b/threat_model.md
index f03b656..34f14c3 100644
--- a/threat_model.md
+++ b/threat_model.md
@@ -122,8 +122,24 @@ If TONs arrive at the wallet address at this time, the message will be applied a
- D: warning about this behavior in technical_notes file for method description
#### Setting the value to "expired" without taking into account the allowable delay
-- P: It is impossible to absolutely precisely synchronize in time with the blockchain, so there is an
+- P: it is impossible to absolutely precisely synchronize in time with the blockchain, so there is an
allowable time delay value. If you get into this gap, the "expired" may be incorrectly set.
- T: double spending for external withdrawals or unnecessary internal withdrawals
- S: check expiration taking into account time delay
-- D: check expiration taking into account time delay
\ No newline at end of file
+- D: check expiration taking into account time delay
+
+#### Repetitive failed transactions burning fees
+- P: with periodic withdrawal cycles, there may be situations where the transaction fails every time.
+ For example, when withdrawing to an uninitialized cold wallet with the bounce flag.
+- T: constant burning of a certain amount of TON on fees
+- S: additional checks to predict the success of the transaction and additional messages in the audit log
+- D: made an additional check on the state of the cold wallet and checking the bounce flag for withdrawal
+
+#### Too frequent withdrawals from a hot wallet to a cold wallet
+- P: if you set only the maximum cutoff for funds on the hot wallet, then the withdrawal to the cold wallet will occur
+ if this amount is exceeded, even if the amount of the excess is less than the amount of the withdrawal fee
+- T: there may be withdrawals of the amount of funds at which the amount of funds is unreasonably small,
+ which will lead to unnecessary burning of funds on fees
+- S: it is necessary to set some delta between the amount of triggering the withdrawal to the cold wallet and the
+ amount that will remain after the withdrawal
+- D: one more parameter has been added to the cutoffs - `hot_wallet_residual_balance`
\ No newline at end of file
diff --git a/todo_list.md b/todo_list.md
index 86da434..7b99730 100644
--- a/todo_list.md
+++ b/todo_list.md
@@ -48,9 +48,15 @@
- [x] Add filling deposit with bounce to test plan
- [x] Update to tonutils-go 1.6.2
- [x] Process masterchain addresses for external incomes
+- [x] Cold wallet withdrawal fix
+- [x] Add hysteresis to cold wallet withdrawal
+- [x] Add user id to notifications
+- [x] Add transaction hash to notifications
+- [ ] Avoid blocking withdrawals to an address if there is a very large amount in the queue for withdrawals to this address
+- [ ] Save tx hash to DB
+- [ ] Support DNS names in recipient address
- [ ] Jetton threat model
- [ ] TNX compatibility test
-- [ ] Support DNS names in recipient address
- [ ] Installation video manual
- [ ] Use stable branch for emulator
- [ ] Download blockchain config at start
@@ -63,3 +69,6 @@
- [ ] Not process removed Jettons
- [ ] Separate .env files for services
- [ ] Automatic migrations
+- [ ] SDK
+- [ ] migration from blueprint to openapi
+- [ ] refactor config and cutoff parameters