Skip to content

Commit

Permalink
Merge pull request #8 from gobicycle/develop
Browse files Browse the repository at this point in the history
v0.4.0
  • Loading branch information
gobicycle authored Jul 6, 2023
2 parents f4ec2a0 + 166fb0d commit eaad5b7
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 59 deletions.
58 changes: 37 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=`. <br/>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: <br/>`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: <br/>`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`, <br/>example: `TGR:kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0:1000000:100000` |
| `TON_CUTOFFS` | cutoffs in nanoTONs in format: <br/>`hot_wallet_min_balance:hot_wallet_max_balance:min_withdrawal_amount`, <br/> 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=`. <br/>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: <br/>`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: <br/>`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`, <br/>example: `TGR:kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0:1000000:100000` |
| `TON_CUTOFFS` | cutoffs in nanoTONs in format: <br/>`hot_wallet_min_balance:hot_wallet_max_balance:min_withdrawal_amount:hot_wallet_residual_balance`, <br/> 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!**
Expand Down Expand Up @@ -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"
}
```

Expand All @@ -146,7 +160,9 @@ from the deposit):
{
"deposit_address":"0QCdsj-u39qVlfYdpPKuAY0hTe5VIsiJcpB5Rx4tOUOyBFhL",
"time": 12345678,
"amount":"200"
"amount":"200",
"tx_hash": "f9b9e7efd3a38da318a894576499f0b6af5ca2da97ccd15c5f1d291a808a0ebf",
"user_id": "123"
}
```

Expand Down
4 changes: 2 additions & 2 deletions blockchain/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/processor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 33 additions & 9 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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]
Expand All @@ -135,19 +139,29 @@ 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
}

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 {
Expand All @@ -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(),
}
}
Loading

0 comments on commit eaad5b7

Please sign in to comment.