diff --git a/api/openapi.json b/api/openapi.json index a3a21c62d..a3228e2d5 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -1223,6 +1223,15 @@ "LiquidityDeposit": { "$ref": "#/components/schemas/LiquidityDepositAction" }, + "LoanIssue": { + "$ref": "#/components/schemas/LoanIssueAction" + }, + "LoanRepayment": { + "$ref": "#/components/schemas/LoanRepaymentAction" + }, + "LoanRequest": { + "$ref": "#/components/schemas/LoanRequestAction" + }, "NftItemTransfer": { "$ref": "#/components/schemas/NftItemTransferAction" }, @@ -1232,6 +1241,9 @@ "OracleRequest": { "$ref": "#/components/schemas/OracleRequestAction" }, + "PoolRoundTransition": { + "$ref": "#/components/schemas/PoolRoundTransitionAction" + }, "Purchase": { "$ref": "#/components/schemas/PurchaseAction" }, @@ -1318,6 +1330,10 @@ "BuyXTR", "DepositXTR", "WithdrawXTR", + "LoanRequest", + "LoanIssue", + "LoanRepayment", + "PoolRoundTransition", "Unknown" ], "example": "TonTransfer", @@ -4891,6 +4907,213 @@ ], "type": "object" }, + "LoanIssueAction": { + "description": "a staking pool lending to a validator, which stakes the loan with the elector", + "properties": { + "accrued": { + "description": "pool coins added to the loan beyond what was asked for, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "amount": { + "description": "loan granted, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "borrower": { + "$ref": "#/components/schemas/AccountAddress" + }, + "collateral": { + "description": "the borrower's own coins staked alongside the loan, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "implementation": { + "$ref": "#/components/schemas/PoolImplementationType" + }, + "loan": { + "$ref": "#/components/schemas/AccountAddress", + "description": "contract that holds the loan and stakes it" + }, + "min_payment": { + "description": "least the borrower pays the pool from the round's reward, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "pool": { + "$ref": "#/components/schemas/AccountAddress" + }, + "round_since": { + "description": "start of the validation round the loan is for, as its unix time", + "example": 1792102152, + "format": "int64", + "type": "integer" + } + }, + "required": [ + "borrower", + "pool", + "loan", + "round_since", + "amount", + "accrued", + "collateral", + "min_payment", + "implementation" + ], + "type": "object" + }, + "LoanRepaymentAction": { + "description": "a loan coming back from the elector and its reward being split", + "properties": { + "accrued": { + "description": "pool coins added to the loan beyond what was asked for, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "amount": { + "description": "loan that was granted, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "borrower": { + "$ref": "#/components/schemas/AccountAddress" + }, + "borrower_payout": { + "description": "returned to the borrower: collateral and the borrower's reward, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "burn_amount": { + "description": "taken from the borrower's reward to be burned, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "implementation": { + "$ref": "#/components/schemas/PoolImplementationType" + }, + "loan": { + "$ref": "#/components/schemas/AccountAddress" + }, + "pool": { + "$ref": "#/components/schemas/AccountAddress" + }, + "pool_reward": { + "description": "what the pool's stakers earned, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "protocol_fee": { + "description": "the pool operator's share of the pool's reward, in nanograms; the operator may also receive what is left over from fees", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "recovered": { + "description": "what came back to the pool: the stake and the reward, less what the loan contract spent to forward it, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "round_since": { + "description": "start of the validation round the loan is for, as its unix time", + "example": 1792102152, + "format": "int64", + "type": "integer" + }, + "shortfall": { + "description": "part of the loan the elector did not return and the collateral could not cover, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + } + }, + "required": [ + "borrower", + "pool", + "loan", + "round_since", + "amount", + "accrued", + "recovered", + "pool_reward", + "protocol_fee", + "borrower_payout", + "burn_amount", + "implementation" + ], + "type": "object" + }, + "LoanRequestAction": { + "description": "a validator asking a staking pool for a loan to stake in the next validation round", + "properties": { + "amount": { + "description": "loan asked for, in nanograms; absent if too large to represent", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "borrower": { + "$ref": "#/components/schemas/AccountAddress" + }, + "implementation": { + "$ref": "#/components/schemas/PoolImplementationType" + }, + "min_payment": { + "description": "least the borrower promises to pay the pool from the round's reward, in nanograms; absent if too large to represent", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "pool": { + "$ref": "#/components/schemas/AccountAddress" + }, + "round_since": { + "description": "start of the validation round the loan is for, as its unix time; 0 if the round is unknown", + "example": 1792102152, + "format": "int64", + "type": "integer" + }, + "ton_attached": { + "description": "sent with the request: the borrower's collateral and the pool's fees, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + } + }, + "required": [ + "borrower", + "pool", + "round_since", + "ton_attached", + "implementation" + ], + "type": "object" + }, "MarketTonRates": { "properties": { "last_date_update": { @@ -6273,6 +6496,58 @@ ], "type": "object" }, + "PoolRoundTransition": { + "description": "election: the pool decided which loans to make and staked them with the elector; began: the validation round started; validated: the round's validators finished validating and the stakes are held until the elector releases them; ended: the round's stakes were released and the pool is recovering them; settled: every loan was recovered and the round's deposits and withdrawals were settled\n", + "enum": [ + "election", + "began", + "validated", + "ended", + "settled" + ], + "type": "string" + }, + "PoolRoundTransitionAction": { + "description": "a staking pool moving one of its validation rounds to its next stage", + "properties": { + "implementation": { + "$ref": "#/components/schemas/PoolImplementationType" + }, + "pool": { + "$ref": "#/components/schemas/AccountAddress" + }, + "round_since": { + "description": "start of the validation round, as its unix time; 0 if the round is unknown", + "example": 1792102152, + "format": "int64", + "type": "integer" + }, + "total_recovered": { + "description": "settled only: recovered from the round, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "total_staked": { + "description": "settled only: lent out for the round, in nanograms", + "example": 1660050553, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "transition": { + "$ref": "#/components/schemas/PoolRoundTransition" + } + }, + "required": [ + "pool", + "round_since", + "transition", + "implementation" + ], + "type": "object" + }, "Price": { "properties": { "currency_type": { diff --git a/api/openapi.yml b/api/openapi.yml index 40593b4cd..57eb04902 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -6872,6 +6872,10 @@ components: - BuyXTR - DepositXTR - WithdrawXTR + - LoanRequest + - LoanIssue + - LoanRepayment + - PoolRoundTransition - Unknown status: type: string @@ -6943,6 +6947,14 @@ components: $ref: '#/components/schemas/DepositXTRAction' BuyXTR: $ref: '#/components/schemas/BuyXTRAction' + LoanRequest: + $ref: '#/components/schemas/LoanRequestAction' + LoanIssue: + $ref: '#/components/schemas/LoanIssueAction' + LoanRepayment: + $ref: '#/components/schemas/LoanRepaymentAction' + PoolRoundTransition: + $ref: '#/components/schemas/PoolRoundTransitionAction' simple_preview: $ref: '#/components/schemas/ActionSimplePreview' base_transactions: @@ -7528,6 +7540,221 @@ components: stake_meta: description: "If present, should be used instead of amount" $ref: '#/components/schemas/Price' + LoanRequestAction: + description: "a validator asking a staking pool for a loan to stake in the next validation round" + type: object + required: + - borrower + - pool + - round_since + - ton_attached + - implementation + properties: + borrower: + $ref: '#/components/schemas/AccountAddress' + pool: + $ref: '#/components/schemas/AccountAddress' + round_since: + description: "start of the validation round the loan is for, as its unix time; 0 if the round is unknown" + type: integer + format: int64 + example: 1792102152 + amount: + description: "loan asked for, in nanograms; absent if too large to represent" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + min_payment: + description: "least the borrower promises to pay the pool from the round's reward, in nanograms; absent if too large to represent" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + ton_attached: + description: "sent with the request: the borrower's collateral and the pool's fees, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + implementation: + $ref: '#/components/schemas/PoolImplementationType' + LoanIssueAction: + description: "a staking pool lending to a validator, which stakes the loan with the elector" + type: object + required: + - borrower + - pool + - loan + - round_since + - amount + - accrued + - collateral + - min_payment + - implementation + properties: + borrower: + $ref: '#/components/schemas/AccountAddress' + pool: + $ref: '#/components/schemas/AccountAddress' + loan: + description: "contract that holds the loan and stakes it" + $ref: '#/components/schemas/AccountAddress' + round_since: + description: "start of the validation round the loan is for, as its unix time" + type: integer + format: int64 + example: 1792102152 + amount: + description: "loan granted, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + accrued: + description: "pool coins added to the loan beyond what was asked for, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + collateral: + description: "the borrower's own coins staked alongside the loan, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + min_payment: + description: "least the borrower pays the pool from the round's reward, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + implementation: + $ref: '#/components/schemas/PoolImplementationType' + LoanRepaymentAction: + description: "a loan coming back from the elector and its reward being split" + type: object + required: + - borrower + - pool + - loan + - round_since + - amount + - accrued + - recovered + - pool_reward + - protocol_fee + - borrower_payout + - burn_amount + - implementation + properties: + borrower: + $ref: '#/components/schemas/AccountAddress' + pool: + $ref: '#/components/schemas/AccountAddress' + loan: + $ref: '#/components/schemas/AccountAddress' + round_since: + description: "start of the validation round the loan is for, as its unix time" + type: integer + format: int64 + example: 1792102152 + amount: + description: "loan that was granted, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + accrued: + description: "pool coins added to the loan beyond what was asked for, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + recovered: + description: "what came back to the pool: the stake and the reward, less what the loan contract spent to forward it, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + pool_reward: + description: "what the pool's stakers earned, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + protocol_fee: + description: "the pool operator's share of the pool's reward, in nanograms; the operator may also receive what is left over from fees" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + borrower_payout: + description: "returned to the borrower: collateral and the borrower's reward, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + burn_amount: + description: "taken from the borrower's reward to be burned, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + shortfall: + description: "part of the loan the elector did not return and the collateral could not cover, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + implementation: + $ref: '#/components/schemas/PoolImplementationType' + PoolRoundTransition: + type: string + description: > + election: the pool decided which loans to make and staked them with the elector; + began: the validation round started; + validated: the round's validators finished validating and the stakes are held until the elector releases them; + ended: the round's stakes were released and the pool is recovering them; + settled: every loan was recovered and the round's deposits and withdrawals were settled + enum: + - election + - began + - validated + - ended + - settled + PoolRoundTransitionAction: + description: "a staking pool moving one of its validation rounds to its next stage" + type: object + required: + - pool + - round_since + - transition + - implementation + properties: + pool: + $ref: '#/components/schemas/AccountAddress' + round_since: + description: "start of the validation round, as its unix time; 0 if the round is unknown" + type: integer + format: int64 + example: 1792102152 + transition: + $ref: '#/components/schemas/PoolRoundTransition' + total_staked: + description: "settled only: lent out for the round, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + total_recovered: + description: "settled only: recovered from the round, in nanograms" + type: integer + format: int64 + x-js-format: bigint + example: 1660050553 + implementation: + $ref: '#/components/schemas/PoolImplementationType' ElectionsRecoverStakeAction: type: object required: diff --git a/pkg/api/event_converters.go b/pkg/api/event_converters.go index 1f94409cf..822fd3e08 100644 --- a/pkg/api/event_converters.go +++ b/pkg/api/event_converters.go @@ -1217,6 +1217,14 @@ func (h *Handler) convertAction(ctx context.Context, viewer *tongo.AccountID, a action.DepositXTR, action.SimplePreview = h.convertDepositXTRAction(ctx, a.DepositXTR, acceptLanguage.Value, viewer) case bath.WithdrawXTR: action.WithdrawXTR, action.SimplePreview = h.convertWithdrawXTRAction(ctx, a.WithdrawXTR, acceptLanguage.Value, viewer) + case bath.LoanRequest: + action.LoanRequest, action.SimplePreview = h.convertLoanRequestAction(a.LoanRequest, acceptLanguage.Value, viewer) + case bath.LoanIssue: + action.LoanIssue, action.SimplePreview = h.convertLoanIssueAction(a.LoanIssue, acceptLanguage.Value, viewer) + case bath.LoanRepayment: + action.LoanRepayment, action.SimplePreview = h.convertLoanRepaymentAction(a.LoanRepayment, acceptLanguage.Value, viewer) + case bath.PoolRoundTransition: + action.PoolRoundTransition, action.SimplePreview = h.convertPoolRoundTransitionAction(a.PoolRoundTransition, acceptLanguage.Value, viewer) } return action, nil } diff --git a/pkg/api/i18n/translations/active.en.toml b/pkg/api/i18n/translations/active.en.toml index ad1d2ef4a..e2c79fe8b 100644 --- a/pkg/api/i18n/translations/active.en.toml +++ b/pkg/api/i18n/translations/active.en.toml @@ -20,3 +20,14 @@ gramTransferAction = "Transferring {{.Value}}" withdrawStakeAction = "Withdraw {{.Value}} from staking pool" withdrawStakeRequestAction = "Request to withdraw {{.Value}} from staking pool" purchaseAction = "Payment for invoice #{{.InvoiceID}}" +loanRequestAction = "Requesting a loan of {{.Amount}} for the validation round starting {{.Round}}" +loanRequestActionNoAmount = "Requesting a loan for the validation round starting {{.Round}}" +loanRequestActionNoRound = "Requesting a loan of {{.Amount}}" +loanRequestActionNoAmountNoRound = "Requesting a loan" +loanIssueAction = "Lending {{.Amount}} to a validator for the round starting {{.Round}}" +loanRepaymentAction = "Loan of {{.Amount}} repaid, stakers earned {{.Reward}}" +poolRoundElection = "Loans decided for the validation round starting {{.Round}}" +poolRoundBegan = "Validation round starting {{.Round}} began" +poolRoundValidated = "Validation round starting {{.Round}} finished, its stakes are held" +poolRoundEnded = "Stakes of the validation round starting {{.Round}} released" +poolRoundSettled = "Validation round starting {{.Round}} settled" diff --git a/pkg/api/i18n/translations/active.ru.toml b/pkg/api/i18n/translations/active.ru.toml index 29a5b12c4..a48179bd9 100644 --- a/pkg/api/i18n/translations/active.ru.toml +++ b/pkg/api/i18n/translations/active.ru.toml @@ -85,3 +85,47 @@ other = "Продление домена {{.Value}}" [purchaseAction] hash = "sha1-980bac6469ccaf2a7cb30f4969696164587599aa" other = "Оплата инвойса #{{.InvoiceID}}" + +[loanRequestAction] +hash = "sha1-0219cb6c8a822e50f1a9851363d22381cc5bac36" +other = "Запрос займа {{.Amount}} на раунд валидации с {{.Round}}" + +[loanRequestActionNoAmount] +hash = "sha1-76f7a8ce80a985773b4d91f276d88fdb75035eff" +other = "Запрос займа на раунд валидации с {{.Round}}" + +[loanRequestActionNoRound] +hash = "sha1-3d60b7a1b7bc3cbe8be69e6e714cfbd7b7d537f9" +other = "Запрос займа {{.Amount}}" + +[loanRequestActionNoAmountNoRound] +hash = "sha1-e4d6e764b0b1db536309320874a24b4c4959338b" +other = "Запрос займа" + +[loanIssueAction] +hash = "sha1-3d379be019c3b56497df2dec3d745ad0e5d380a8" +other = "Заём {{.Amount}} валидатору на раунд с {{.Round}}" + +[loanRepaymentAction] +hash = "sha1-c302addb6b3820552191fac6d50d3487f86589e2" +other = "Заём {{.Amount}} погашен, стейкеры заработали {{.Reward}}" + +[poolRoundElection] +hash = "sha1-359508a03cbb4e6fd22da276d0c5971044448257" +other = "Займы на раунд валидации с {{.Round}} распределены" + +[poolRoundBegan] +hash = "sha1-8040e6a6ad559ff686898e199df98b0aa6bd01b3" +other = "Раунд валидации с {{.Round}} начался" + +[poolRoundValidated] +hash = "sha1-65d7ffdec8e38d6d18ab67ef7bb6c36f1a5d781d" +other = "Раунд валидации с {{.Round}} завершён, стейки удерживаются" + +[poolRoundEnded] +hash = "sha1-4bd25ed40dd559c31248ac93fa76fad1a255e265" +other = "Стейки раунда валидации с {{.Round}} освобождены" + +[poolRoundSettled] +hash = "sha1-99e721d3c3f554f301b73e2816712baf3690c388" +other = "Раунд валидации с {{.Round}} рассчитан" diff --git a/pkg/api/loan_converters.go b/pkg/api/loan_converters.go new file mode 100644 index 000000000..cb97ff2d3 --- /dev/null +++ b/pkg/api/loan_converters.go @@ -0,0 +1,173 @@ +package api + +import ( + "time" + + "github.com/tonkeeper/opentonapi/pkg/api/i18n" + "github.com/tonkeeper/opentonapi/pkg/bath" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/tongo" +) + +var poolRoundTransitionMessages = map[bath.PoolRoundTransitionType]i18n.M{ + bath.PoolRoundElection: {ID: "poolRoundElection", Other: "Loans decided for the validation round starting {{.Round}}"}, + bath.PoolRoundBegan: {ID: "poolRoundBegan", Other: "Validation round starting {{.Round}} began"}, + bath.PoolRoundValidated: {ID: "poolRoundValidated", Other: "Validation round starting {{.Round}} finished, its stakes are held"}, + bath.PoolRoundEnded: {ID: "poolRoundEnded", Other: "Stakes of the validation round starting {{.Round}} released"}, + bath.PoolRoundSettled: {ID: "poolRoundSettled", Other: "Validation round starting {{.Round}} settled"}, +} + +func (h *Handler) convertLoanRequestAction(a *bath.LoanRequestAction, acceptLanguage string, viewer *tongo.AccountID) (oas.OptLoanRequestAction, oas.ActionSimplePreview) { + request := oas.LoanRequestAction{ + Borrower: convertAccountAddress(a.Borrower, h.addressBook), + Pool: convertAccountAddress(a.Pool, h.addressBook), + RoundSince: int64(a.RoundSince), + TonAttached: a.TonAttached, + Implementation: oas.PoolImplementationType(a.Implementation), + } + if a.Amount != nil { + request.Amount = oas.NewOptInt64(*a.Amount) + } + if a.MinPayment != nil { + request.MinPayment = oas.NewOptInt64(*a.MinPayment) + } + var action oas.OptLoanRequestAction + action.SetTo(request) + preview := oas.ActionSimplePreview{ + Name: "Loan Request", + Accounts: distinctAccounts(viewer, h.addressBook, &a.Borrower, &a.Pool), + } + data := i18n.Template{} + if a.Amount != nil { + value := i18n.FormatGrams(*a.Amount) + data["Amount"] = value + preview.Value = oas.NewOptString(value) + } + // a round of unknown start is left out rather than dated 1970 + if a.RoundSince != 0 { + data["Round"] = roundStart(a.RoundSince) + } + var message i18n.M + switch { + case a.Amount == nil && a.RoundSince == 0: + message = i18n.M{ID: "loanRequestActionNoAmountNoRound", Other: "Requesting a loan"} + case a.Amount == nil: + message = i18n.M{ID: "loanRequestActionNoAmount", Other: "Requesting a loan for the validation round starting {{.Round}}"} + case a.RoundSince == 0: + message = i18n.M{ID: "loanRequestActionNoRound", Other: "Requesting a loan of {{.Amount}}"} + default: + message = i18n.M{ID: "loanRequestAction", Other: "Requesting a loan of {{.Amount}} for the validation round starting {{.Round}}"} + } + preview.Description = i18n.T(acceptLanguage, i18n.C{ + DefaultMessage: &message, + TemplateData: data, + }) + return action, preview +} + +func (h *Handler) convertLoanIssueAction(a *bath.LoanIssueAction, acceptLanguage string, viewer *tongo.AccountID) (oas.OptLoanIssueAction, oas.ActionSimplePreview) { + var action oas.OptLoanIssueAction + action.SetTo(oas.LoanIssueAction{ + Borrower: convertAccountAddress(a.Borrower, h.addressBook), + Pool: convertAccountAddress(a.Pool, h.addressBook), + Loan: convertAccountAddress(a.Loan, h.addressBook), + RoundSince: int64(a.RoundSince), + Amount: a.Amount, + Accrued: a.Accrued, + Collateral: a.Collateral, + MinPayment: a.MinPayment, + Implementation: oas.PoolImplementationType(a.Implementation), + }) + value := i18n.FormatGrams(a.Amount + a.Accrued) + return action, oas.ActionSimplePreview{ + Name: "Loan Issue", + Description: i18n.T(acceptLanguage, i18n.C{ + DefaultMessage: &i18n.M{ + ID: "loanIssueAction", + Other: "Lending {{.Amount}} to a validator for the round starting {{.Round}}", + }, + TemplateData: i18n.Template{"Amount": value, "Round": roundStart(a.RoundSince)}, + }), + Value: oas.NewOptString(value), + Accounts: distinctAccounts(viewer, h.addressBook, &a.Pool, &a.Borrower, &a.Loan), + } +} + +func (h *Handler) convertLoanRepaymentAction(a *bath.LoanRepaymentAction, acceptLanguage string, viewer *tongo.AccountID) (oas.OptLoanRepaymentAction, oas.ActionSimplePreview) { + repayment := oas.LoanRepaymentAction{ + Borrower: convertAccountAddress(a.Borrower, h.addressBook), + Pool: convertAccountAddress(a.Pool, h.addressBook), + Loan: convertAccountAddress(a.Loan, h.addressBook), + RoundSince: int64(a.RoundSince), + Amount: a.Amount, + Accrued: a.Accrued, + Recovered: a.Recovered, + PoolReward: a.PoolReward, + ProtocolFee: a.ProtocolFee, + BorrowerPayout: a.BorrowerPayout, + BurnAmount: a.BurnAmount, + Implementation: oas.PoolImplementationType(a.Implementation), + } + if a.Shortfall != nil { + repayment.Shortfall = oas.NewOptInt64(*a.Shortfall) + } + var action oas.OptLoanRepaymentAction + action.SetTo(repayment) + loan := i18n.FormatGrams(a.Amount + a.Accrued) + reward := i18n.FormatGrams(a.PoolReward) + return action, oas.ActionSimplePreview{ + Name: "Loan Repayment", + Description: i18n.T(acceptLanguage, i18n.C{ + DefaultMessage: &i18n.M{ + ID: "loanRepaymentAction", + Other: "Loan of {{.Amount}} repaid, stakers earned {{.Reward}}", + }, + TemplateData: i18n.Template{"Amount": loan, "Reward": reward}, + }), + Value: oas.NewOptString(reward), + Accounts: distinctAccounts(viewer, h.addressBook, &a.Pool, &a.Borrower, &a.Loan), + } +} + +func (h *Handler) convertPoolRoundTransitionAction(a *bath.PoolRoundTransitionAction, acceptLanguage string, viewer *tongo.AccountID) (oas.OptPoolRoundTransitionAction, oas.ActionSimplePreview) { + transition := oas.PoolRoundTransitionAction{ + Pool: convertAccountAddress(a.Pool, h.addressBook), + RoundSince: int64(a.RoundSince), + Transition: oas.PoolRoundTransition(a.Transition), + Implementation: oas.PoolImplementationType(a.Implementation), + } + if a.TotalStaked != nil { + transition.TotalStaked = oas.NewOptInt64(*a.TotalStaked) + } + if a.TotalRecovered != nil { + transition.TotalRecovered = oas.NewOptInt64(*a.TotalRecovered) + } + var action oas.OptPoolRoundTransitionAction + action.SetTo(transition) + preview := oas.ActionSimplePreview{ + Name: "Validation Round", + Accounts: distinctAccounts(viewer, h.addressBook, &a.Pool), + } + // a round of unknown start is left undescribed rather than dated 1970 + if message, ok := poolRoundTransitionMessages[a.Transition]; ok && a.RoundSince != 0 { + preview.Description = i18n.T(acceptLanguage, i18n.C{ + DefaultMessage: &message, + TemplateData: i18n.Template{"Round": roundStart(a.RoundSince)}, + }) + } + if a.TotalStaked != nil && a.TotalRecovered != nil { + preview.Value = oas.NewOptString(formatSignedGrams(*a.TotalRecovered - *a.TotalStaked)) + } + return action, preview +} + +func roundStart(roundSince uint32) string { + return time.Unix(int64(roundSince), 0).UTC().Format("2006-01-02 15:04 UTC") +} + +func formatSignedGrams(amount int64) string { + if amount < 0 { + return "-" + i18n.FormatGrams(-amount) + } + return i18n.FormatGrams(amount) +} diff --git a/pkg/api/loan_converters_test.go b/pkg/api/loan_converters_test.go new file mode 100644 index 000000000..bce184e4f --- /dev/null +++ b/pkg/api/loan_converters_test.go @@ -0,0 +1,81 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tonkeeper/opentonapi/pkg/addressbook" + "github.com/tonkeeper/opentonapi/pkg/bath" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/ton" +) + +func TestConvertPoolRoundTransitionActionValue(t *testing.T) { + h := &Handler{ + addressBook: mockAddressBook{ + OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { + return addressbook.KnownAddress{}, false + }, + }, + } + tests := []struct { + name string + staked int64 + recovered int64 + want string + }{ + {name: "gain", staked: 1_000_000_000_000, recovered: 1_234_567_890_123, want: "234 Gram"}, + {name: "loss", staked: 1_234_567_890_123, recovered: 0, want: "-1 234 Gram"}, + {name: "fractional loss", staked: 1_000_000_000, recovered: 500_000_000, want: "-0.5 Gram"}, + {name: "even", staked: 1_000_000_000, recovered: 1_000_000_000, want: "0 Gram"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &bath.PoolRoundTransitionAction{ + Pool: ton.MustParseAccountID("0:3996a30dd8d4728d84d8596730bff7d47e2c8ef88de888bf2f6c5aef8df134dc"), + Transition: bath.PoolRoundSettled, + TotalStaked: &tt.staked, + TotalRecovered: &tt.recovered, + } + _, preview := h.convertPoolRoundTransitionAction(a, "en", nil) + require.Equal(t, tt.want, preview.Value.Value) + }) + } +} + +func TestConvertLoanRequestActionDescription(t *testing.T) { + h := &Handler{ + addressBook: mockAddressBook{ + OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { + return addressbook.KnownAddress{}, false + }, + }, + } + amount := int64(1_000_000_000_000) + tests := []struct { + name string + amount *int64 + roundSince uint32 + lang string + want string + }{ + {name: "amount and round", amount: &amount, roundSince: 1_700_000_000, lang: "en", want: "Requesting a loan of 1 000 Gram for the validation round starting 2023-11-14 22:13 UTC"}, + {name: "round only", roundSince: 1_700_000_000, lang: "en", want: "Requesting a loan for the validation round starting 2023-11-14 22:13 UTC"}, + {name: "amount only", amount: &amount, lang: "en", want: "Requesting a loan of 1 000 Gram"}, + {name: "neither", lang: "en", want: "Requesting a loan"}, + {name: "amount only ru", amount: &amount, lang: "ru", want: "Запрос займа 1 000 Gram"}, + {name: "neither ru", lang: "ru", want: "Запрос займа"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &bath.LoanRequestAction{ + Borrower: ton.MustParseAccountID("0:3996a30dd8d4728d84d8596730bff7d47e2c8ef88de888bf2f6c5aef8df134dc"), + Pool: ton.MustParseAccountID("0:3996a30dd8d4728d84d8596730bff7d47e2c8ef88de888bf2f6c5aef8df134dc"), + RoundSince: tt.roundSince, + Amount: tt.amount, + } + _, preview := h.convertLoanRequestAction(a, tt.lang, nil) + require.Equal(t, tt.want, preview.Description) + }) + } +} diff --git a/pkg/bath/actions.go b/pkg/bath/actions.go index 9529cc462..c04020078 100644 --- a/pkg/bath/actions.go +++ b/pkg/bath/actions.go @@ -54,6 +54,10 @@ const ( BuyXTR ActionType = "BuyXTR" DepositXTR ActionType = "DepositXTR" WithdrawXTR ActionType = "WithdrawXTR" + LoanRequest ActionType = "LoanRequest" + LoanIssue ActionType = "LoanIssue" + LoanRepayment ActionType = "LoanRepayment" + PoolRoundTransition ActionType = "PoolRoundTransition" Unknown ActionType = "Unknown" ) @@ -112,6 +116,10 @@ type ( BuyXTR *BuyXTRAction `json:",omitempty"` DepositXTR *DepositXTRAction `json:",omitempty"` WithdrawXTR *WithdrawXTRAction `json:",omitempty"` + LoanRequest *LoanRequestAction `json:",omitempty"` + LoanIssue *LoanIssueAction `json:",omitempty"` + LoanRepayment *LoanRepaymentAction `json:",omitempty"` + PoolRoundTransition *PoolRoundTransitionAction `json:",omitempty"` Success bool Type ActionType Error *string `json:",omitempty"` @@ -444,6 +452,14 @@ func (a Action) ContributeToExtra(account tongo.AccountID) int64 { return 0 case WithdrawStake: return detectDirection(account, a.WithdrawStake.Pool, a.WithdrawStake.Staker, a.WithdrawStake.Amount) + case LoanRequest: + return a.LoanRequest.contributeToExtra(account) + case LoanIssue: + return a.LoanIssue.contributeToExtra(account) + case LoanRepayment: + return a.LoanRepayment.contributeToExtra(account) + case PoolRoundTransition: + return a.PoolRoundTransition.contributeToExtra(account) case LiquidityDeposit: extra := int64(0) for _, token := range a.LiquidityDepositAction.Tokens { @@ -488,6 +504,10 @@ func (a Action) IsSubject(account tongo.AccountID) bool { a.RemoveExtension, a.SetSignatureAllowed, a.LiquidityDepositAction, + a.LoanRequest, + a.LoanIssue, + a.LoanRepayment, + a.PoolRoundTransition, } { if i != nil && !reflect.ValueOf(i).IsNil() { return slices.Contains(i.SubjectAccounts(), account) diff --git a/pkg/bath/bath_test.go b/pkg/bath/bath_test.go index 1d6333e48..8fc62e1c6 100644 --- a/pkg/bath/bath_test.go +++ b/pkg/bath/bath_test.go @@ -484,6 +484,24 @@ func TestFindActions(t *testing.T) { "9236ab916e92293a822e1bb701c970de97eac3df18f1a18bfca3a4d3fd049229": { tongo.MustParseBlockID("(0,c000000000000000,88394637)"), }, + "421fa03279386d307ad993e1fc131d2e5de9da85478755b35beb98d037f90f8e": { + tongo.MustParseBlockID("(0,8000000000000000,103773211)"), + }, + "046a4f3214343224ea7406f15767ef4fdbb50ec75e7b8976fc9f0bcbba093a90": { + tongo.MustParseBlockID("(0,8000000000000000,95501781)"), + tongo.MustParseBlockID("(-1,8000000000000000,91096683)"), + }, + "57074455cf497e7cded8d790b8de2d6c7e5d6148252565de1d5eeee25020fc5d": { + tongo.MustParseBlockID("(0,8000000000000000,103852891)"), + }, + "87a89f582a6d372beece6a29aa5d306240065fdfe00897263fd5925d0e248e60": { + tongo.MustParseBlockID("(0,8000000000000000,103852891)"), + }, + "d8d505bceb48b1916f7211faa604168d42f10932ab939f831515c84a2b5d2de6": { + tongo.MustParseBlockID("(0,8000000000000000,103771382)"), + tongo.MustParseBlockID("(-1,8000000000000000,99187813)"), + tongo.MustParseBlockID("(0,8000000000000000,103771385)"), + }, } type Case struct { @@ -1298,6 +1316,40 @@ func TestFindActions(t *testing.T) { filenamePrefix: "hipo-deferred-unstake", hash: "9236ab916e92293a822e1bb701c970de97eac3df18f1a18bfca3a4d3fd049229", }, + { + // A validator asks the treasury for a loan for the next round. + name: "hipo loan request", + filenamePrefix: "hipo-loan-request", + hash: "421fa03279386d307ad993e1fc131d2e5de9da85478755b35beb98d037f90f8e", + }, + { + // Bidding closes: the treasury decides the round's loans and stakes each one, and + // its logs say which borrower each loan is for. A round with one loan: two loans + // staked in one masterchain block send new_stake at the same lt, and the test + // storage then resolves both to the first elector transaction. + name: "hipo round election", + filenamePrefix: "hipo-round-election", + hash: "046a4f3214343224ea7406f15767ef4fdbb50ec75e7b8976fc9f0bcbba093a90", + }, + { + // The validator set changes: the round before stops validating ... + name: "hipo round validated", + filenamePrefix: "hipo-round-validated", + hash: "57074455cf497e7cded8d790b8de2d6c7e5d6148252565de1d5eeee25020fc5d", + }, + { + // ... and, in a second message, the new round begins. + name: "hipo round began", + filenamePrefix: "hipo-round-began", + hash: "87a89f582a6d372beece6a29aa5d306240065fdfe00897263fd5925d0e248e60", + }, + { + // The stakes are released: each loan is repaid and split by its log, the borrower + // fee goes on to the burner, and the round's bills are burned. + name: "hipo round end", + filenamePrefix: "hipo-round-end", + hash: "d8d505bceb48b1916f7211faa604168d42f10932ab939f831515c84a2b5d2de6", + }, } var sharedStorage *litestorage.LiteStorage diff --git a/pkg/bath/bubble.go b/pkg/bath/bubble.go index 0c665a39a..801c045d9 100644 --- a/pkg/bath/bubble.go +++ b/pkg/bath/bubble.go @@ -61,6 +61,7 @@ func fromTrace(trace *core.Trace, parent *core.Trace) *Bubble { external: trace.InMsg == nil || trace.InMsg.IsExternal(), accountWasActiveAtComputingTime: trace.Type != core.OrdinaryTx || trace.ComputePhase == nil || trace.ComputePhase.SkipReason != tlb.ComputeSkipReasonNoState, additionalInfo: trace.AdditionalInfo(), + utime: trace.Utime, } accounts := []tongo.AccountID{trace.Account} @@ -87,6 +88,7 @@ func fromTrace(trace *core.Trace, parent *core.Trace) *Bubble { btx.decodedBody = msg.DecodedBody btx.inputFrom = source btx.init = msg.Init + btx.inputLt = msg.CreatedLt initInterfaces = msg.InitInterfaces } var inputAmount int64 @@ -139,6 +141,10 @@ func fromTrace(trace *core.Trace, parent *core.Trace) *Bubble { } b.Children[i] = fromTrace(c, trace) + if child, ok := b.Children[i].Info.(BubbleTx); ok && len(btx.externalOut) > 0 { + child.senderExternalOut = btx.externalOut + b.Children[i].Info = child + } } if actionPhase := trace.Transaction.ActionPhase; actionPhase != nil { aggregatedFee += int64(actionPhase.FwdFees) diff --git a/pkg/bath/bubble_tx.go b/pkg/bath/bubble_tx.go index b8741e33a..bc91d7922 100644 --- a/pkg/bath/bubble_tx.go +++ b/pkg/bath/bubble_tx.go @@ -21,6 +21,12 @@ type BubbleTx struct { decodedBody *core.DecodedMessageBody init []byte externalOut []core.Message + utime int64 + // inputLt and senderExternalOut let a straw read what the sender logged about this + // message: contracts that log write the log next to the message it describes, in the + // sender's transaction. + inputLt uint64 + senderExternalOut []core.Message additionalInfo *core.TraceAdditionalInfo accountWasActiveAtComputingTime bool diff --git a/pkg/bath/hipo.go b/pkg/bath/hipo.go index 5075c7098..3e072bfd4 100644 --- a/pkg/bath/hipo.go +++ b/pkg/bath/hipo.go @@ -170,6 +170,10 @@ func hipoDepositComment(bubble *Bubble) bool { return HasTextComment("d")(bubble) || HasTextComment("D")(bubble) } +func hipoUnstakeAllComment(bubble *Bubble) bool { + return HasTextComment("w")(bubble) || HasTextComment("W")(bubble) +} + func hipoDepositBuilder(newAction *BubbleDepositStake, bubble *Bubble) error { tx := bubble.Info.(BubbleTx) newAction.Pool = tx.account.Address @@ -269,6 +273,36 @@ var DepositHipoStakeDeferredStraw = Straw[BubbleDepositStake]{ }, } +// WithdrawHipoStakeAllRequestStraw is the request to unstake a whole balance, sent as +// send_unstake_all or as the text comment "w". It names no amount: the wallet burns whatever it +// holds, and that burn and its payout are actions of their own below it. +// +// send_unstake_all -> treasury -> proxy_unstake_all -> parent -> unstake_all -> hGRAM wallet +var WithdrawHipoStakeAllRequestStraw = Straw[BubbleWithdrawStakeRequest]{ + CheckFuncs: []bubbleCheck{IsTx, IsAccount(references.HipoTreasury), Or( + HasOperation(abi.HipoFinanceSendUnstakeAllMsgOp), + hipoUnstakeAllComment, + )}, + Builder: func(newAction *BubbleWithdrawStakeRequest, bubble *Bubble) error { + tx := bubble.Info.(BubbleTx) + newAction.Pool = tx.account.Address + newAction.Success = tx.success + newAction.Implementation = core.StakingImplementationHipo + if tx.inputFrom != nil { + newAction.Staker = tx.inputFrom.Address + } + return nil + }, + SingleChild: &Straw[BubbleWithdrawStakeRequest]{ + CheckFuncs: []bubbleCheck{IsTx, IsAccount(references.HipoParent), HasOperation(abi.HipoFinanceProxyUnstakeAllMsgOp)}, + Optional: true, + SingleChild: &Straw[BubbleWithdrawStakeRequest]{ + CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.HipoFinanceUnstakeAllMsgOp)}, + Optional: true, + }, + }, +} + func hipoRolledBack(bubble *Bubble) bool { return HasChild(IsTx, HasOperation(abi.HipoFinanceProxyRollbackUnstakeMsgOp))(bubble) } diff --git a/pkg/bath/hipo_loans.go b/pkg/bath/hipo_loans.go new file mode 100644 index 000000000..6bf4e8b7c --- /dev/null +++ b/pkg/bath/hipo_loans.go @@ -0,0 +1,422 @@ +package bath + +import ( + "math/big" + "slices" + + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/abi" + "github.com/tonkeeper/tongo/tlb" +) + +// Hipo's loans and rounds. The treasury lends to validators each round and reports the +// amounts only in its logs (see hipo_logs.go), so the loan straws are anchored on a log the +// treasury wrote in its own transaction: anyone can send these ops, but only the treasury can +// write its logs. + +const hipoRoundStartedWithin = 65536 / 2 + +// PoolRoundElectionHipoStraw: bidding has closed and the treasury decides the round's loans. +// +// participate_in_election (external) -> treasury -> decide_loan_requests -> treasury -> +// process_loan_requests -> treasury +var PoolRoundElectionHipoStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: hipoTreasuryExternal(abi.HipoFinanceParticipateInElectionExtInMsgOp), + Builder: hipoRoundBuilder(PoolRoundElection), +} + +// PoolRoundBeganHipoStraw: the validator set changed. The treasury is told once for each +// round it takes part in: the round that has just begun, and the one before it, whose +// validators have just stopped. Which one this is shows in how long ago the round started. +var PoolRoundBeganHipoStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: hipoTreasuryExternal(abi.HipoFinanceVsetChangedExtInMsgOp), + Builder: func(newAction *BubblePoolRoundTransition, bubble *Bubble) error { + if err := hipoRoundBuilder(PoolRoundBegan)(newAction, bubble); err != nil { + return err + } + // The change comes moments after the new round starts and a whole round after the + // old one did, so half a round tells them apart even if the message is hours late. + if newAction.RoundSince != 0 && bubble.Info.(BubbleTx).utime-int64(newAction.RoundSince) > hipoRoundStartedWithin { + newAction.Transition = PoolRoundValidated + } + return nil + }, +} + +// PoolRoundEndedHipoStraw: the stakes are released and the treasury asks for them back. +// +// finish_participation (external) -> treasury -> recover_stakes -> treasury -> +// proxy_recover_stake -> each loan +var PoolRoundEndedHipoStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: hipoTreasuryExternal(abi.HipoFinanceFinishParticipationExtInMsgOp), + Builder: hipoRoundBuilder(PoolRoundEnded), +} + +// PoolRoundSettledHipoStraw: every loan is back, and the round's bills are burned. The bills' +// own settlements below stay DepositStake, JettonMint and WithdrawStake actions; the round +// folds in the burning machinery and, from log::finish, the round's totals. +// +// treasury -> burn_all -> collection -> revoke -> bill -> bill_burned -> collection -> +// ... -> last_bill_burned -> treasury +var PoolRoundSettledHipoStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.HipoFinanceBurnAllMsgOp), hipoFromTreasury}, + Builder: func(newAction *BubblePoolRoundTransition, bubble *Bubble) error { + tx := bubble.Info.(BubbleTx) + newAction.Pool = references.HipoTreasury + newAction.Transition = PoolRoundSettled + newAction.Implementation = core.StakingImplementationHipo + newAction.Success = tx.success + newAction.add(bubble) + // When burn_all is sent on recovering the round's last loan, that loan's repayment log + // names the round. last_bill_burned names it too, but may never come. + for _, msg := range tx.senderExternalOut { + if _, value, ok := core.DecodeHipoLog(msg); ok { + if log, ok := value.(core.HipoRepaymentLog); ok { + newAction.RoundSince = log.RoundSince + } + } + } + return nil + }, + SingleChild: hipoOptionalLastBill(), +} + +// LoanRequestHipoStraw: a validator asks for a loan. The treasury accepts any of the request +// layouts it has used, and keeps the coins sent with it as collateral. +var LoanRequestHipoStraw = Straw[BubbleLoanRequest]{ + CheckFuncs: []bubbleCheck{IsTx, IsAccount(references.HipoTreasury), HasAnyOperation( + abi.HipoFinanceRequestLoanMsgOp, abi.HipoFinanceRequestLoanV2MsgOp, + abi.HipoFinanceRequestLoanV3MsgOp, abi.HipoFinanceRequestLoanV4MsgOp, + )}, + Builder: func(newAction *BubbleLoanRequest, bubble *Bubble) error { + tx := bubble.Info.(BubbleTx) + newAction.Pool = tx.account.Address + newAction.Implementation = core.StakingImplementationHipo + newAction.TonAttached = tx.inputAmount + newAction.Success = tx.success + newAction.add(bubble) + if tx.inputFrom != nil { + newAction.Borrower = tx.inputFrom.Address + } + switch body := tx.decodedBody.Value.(type) { + case abi.HipoFinanceRequestLoanMsgBody: + newAction.RoundSince, newAction.Amount, newAction.MinPayment = body.RoundSince, hipoVarCoins(body.LoanAmount), hipoVarCoins(body.MinPayment) + case abi.HipoFinanceRequestLoanV2MsgBody: + newAction.RoundSince, newAction.Amount, newAction.MinPayment = body.RoundSince, hipoVarCoins(body.LoanAmount), hipoVarCoins(body.MinPayment) + case abi.HipoFinanceRequestLoanV3MsgBody: + newAction.RoundSince, newAction.Amount, newAction.MinPayment = body.RoundSince, hipoVarCoins(body.LoanAmount), hipoVarCoins(body.MinPayment) + case abi.HipoFinanceRequestLoanV4MsgBody: + newAction.RoundSince, newAction.Amount, newAction.MinPayment = body.RoundSince, hipoVarCoins(body.LoanAmount), hipoVarCoins(body.MinPayment) + } + return nil + }, + SingleChild: &Straw[BubbleLoanRequest]{ + CheckFuncs: []bubbleCheck{IsTx, IsBounced}, + Builder: addTransfer[BubbleLoanRequest], + Optional: true, + }, +} + +// LoanIssueHipoStraw: the treasury lends, and the loan contract stakes it. The elector leg +// below stays an ElectionsDepositStake. +// +// treasury -> proxy_new_stake -> loan -> new_stake -> elector +var LoanIssueHipoStraw = Straw[BubbleLoanIssue]{ + CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.HipoFinanceProxyNewStakeMsgOp), func(bubble *Bubble) bool { + _, _, ok := hipoIssueLog(bubble.Info.(BubbleTx)) + return ok + }}, + Builder: func(newAction *BubbleLoanIssue, bubble *Bubble) error { + tx := bubble.Info.(BubbleTx) + log, borrower, _ := hipoIssueLog(tx) + newAction.Borrower = borrower + newAction.Pool = references.HipoTreasury + newAction.Loan = tx.account.Address + newAction.Implementation = core.StakingImplementationHipo + newAction.Success = tx.success + newAction.add(bubble) + newAction.RoundSince = log.RoundSince + newAction.Amount = int64(log.LoanAmount) + newAction.Accrued = int64(log.AccrueAmount) + newAction.Collateral = int64(log.StakeAmount) + newAction.MinPayment = int64(log.MinPayment) + hipoCredit(bubble, borrower) + return nil + }, + SingleChild: &Straw[BubbleLoanIssue]{ + CheckFuncs: []bubbleCheck{Is(BubbleContractDeploy{})}, + Optional: true, + }, +} + +// LoanRepaymentHipoStraw: a loan is back from the elector and the treasury splits it, by +// log::repayment, between the stakers, the governor, the burner and the borrower. +// +// loan -> recover_stake_result -> treasury -> loan_result -> borrower +// -> take_borrower_fee -> burner +// -> take_profit -> governor +var LoanRepaymentHipoStraw = Straw[BubbleLoanRepayment]{ + CheckFuncs: []bubbleCheck{IsTx, IsAccount(references.HipoTreasury), HasOperation(abi.HipoFinanceRecoverStakeResultMsgOp), func(bubble *Bubble) bool { + _, _, ok := hipoRepaymentLog(hipoLogs(bubble)) + return ok + }}, + Builder: func(newAction *BubbleLoanRepayment, bubble *Bubble) error { + tx := bubble.Info.(BubbleTx) + logs := hipoLogs(bubble) + log, borrower, _ := hipoRepaymentLog(logs) + newAction.Borrower = borrower + newAction.Pool = tx.account.Address + newAction.Implementation = core.StakingImplementationHipo + newAction.Success = tx.success + newAction.add(bubble) + if tx.inputFrom != nil { + newAction.Loan = tx.inputFrom.Address + } + newAction.RoundSince = log.RoundSince + newAction.Amount = int64(log.LoanAmount) + newAction.Accrued = int64(log.AccrueAmount) + newAction.Recovered = int64(log.RepaymentAmount) + newAction.PoolReward = int64(log.StakersShare) + newAction.ProtocolFee = int64(log.GovernorShare) + newAction.BorrowerPayout = int64(log.BorrowerShare) + newAction.BurnAmount = int64(log.BurnShare) + hipoCredit(bubble, borrower) + for _, value := range logs[core.HipoLogDeficit] { + if deficit, ok := value.(core.HipoDeficitLog); ok { + shortfall := int64(deficit.Shortfall) + newAction.Shortfall = &shortfall + } + } + return nil + }, + Children: []Straw[BubbleLoanRepayment]{ + { + CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.HipoFinanceLoanResultMsgOp), hipoFromTreasury}, + Builder: addTransfer[BubbleLoanRepayment], + Optional: true, + }, + { + CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.HipoFinanceTakeBorrowerFeeMsgOp), hipoFromTreasury}, + Builder: addTransfer[BubbleLoanRepayment], + Optional: true, + }, + { + CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.HipoFinanceTakeProfitMsgOp), hipoFromTreasury}, + Builder: addTransfer[BubbleLoanRepayment], + Optional: true, + }, + }, +} + +// hipoRoundElectionStepStraw folds the treasury's messages to itself into the election round, +// one per pass: there are as many as the round's loans need, since each one stops at a gas +// budget and sends the next. +var hipoRoundElectionStepStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{hipoRoundIs(PoolRoundElection)}, + Builder: hipoKeepRound, + SingleChild: &Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, IsSuccess, IsAccount(references.HipoTreasury), hipoFromTreasury, HasAnyOperation(abi.HipoFinanceDecideLoanRequestsMsgOp, abi.HipoFinanceProcessLoanRequestsMsgOp)}, + Builder: addTransfer[BubblePoolRoundTransition], + }, +} + +// hipoRoundEndedStepStraw folds the treasury's recover_stakes messages to itself into the ended +// round, one per pass, the same way. +var hipoRoundEndedStepStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{hipoRoundIs(PoolRoundEnded)}, + Builder: hipoKeepRound, + SingleChild: &Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, IsSuccess, IsAccount(references.HipoTreasury), hipoFromTreasury, HasOperation(abi.HipoFinanceRecoverStakesMsgOp)}, + Builder: addTransfer[BubblePoolRoundTransition], + }, +} + +// hipoRoundRecoverStraw folds each loan's proxy_recover_stake into the round that asked for +// it; the elector leg below it stays an ElectionsRecoverStake. +var hipoRoundRecoverStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{hipoRoundIs(PoolRoundEnded)}, + Builder: hipoKeepRound, + SingleChild: &Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, IsSuccess, HasOperation(abi.HipoFinanceProxyRecoverStakeMsgOp), hipoFromTreasury}, + Builder: addTransfer[BubblePoolRoundTransition], + }, +} + +// hipoRoundBillStraw folds one burned bill into the settled round, one per pass. +var hipoRoundBillStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{hipoRoundIs(PoolRoundSettled)}, + Builder: hipoKeepRound, + SingleChild: &Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, IsSuccess, HasOperation(abi.SbtRevokeMsgOp)}, + Builder: addTransfer[BubblePoolRoundTransition], + SingleChild: &Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, IsSuccess, HasOperation(abi.HipoFinanceBillBurnedMsgOp)}, + Builder: addTransfer[BubblePoolRoundTransition], + SingleChild: hipoOptionalLastBill(), + }, + }, +} + +// hipoRoundMintStraw folds the treasury's half of a deposit bill's settlement into the settled +// round, one per pass. The hGRAM it mints stays a JettonMint, from JettonMintHipoStraw. +// +// bill_burned -> collection -> mint_tokens -> treasury -> proxy_tokens_minted -> parent -> +// tokens_minted -> hGRAM wallet +var hipoRoundMintStraw = Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{hipoRoundIs(PoolRoundSettled)}, + Builder: hipoKeepRound, + SingleChild: &Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, IsSuccess, IsAccount(references.HipoTreasury), HasOperation(abi.HipoFinanceMintTokensMsgOp)}, + Builder: addTransfer[BubblePoolRoundTransition], + SingleChild: &Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, IsSuccess, IsAccount(references.HipoParent), HasOperation(abi.HipoFinanceProxyTokensMintedMsgOp)}, + Builder: addTransfer[BubblePoolRoundTransition], + }, + }, +} + +var hipoLastBillBurned = Straw[BubblePoolRoundTransition]{ + CheckFuncs: []bubbleCheck{IsTx, IsAccount(references.HipoTreasury), HasOperation(abi.HipoFinanceLastBillBurnedMsgOp)}, + Builder: func(newAction *BubblePoolRoundTransition, bubble *Bubble) error { + tx := bubble.Info.(BubbleTx) + newAction.add(bubble) + if body, ok := tx.decodedBody.Value.(abi.HipoFinanceLastBillBurnedMsgBody); ok { + newAction.RoundSince = body.RoundSince + } + for _, value := range hipoLogs(bubble)[core.HipoLogFinish] { + if log, ok := value.(core.HipoFinishLog); ok { + staked, recovered := int64(log.TotalStaked), int64(log.TotalRecovered) + newAction.RoundSince = log.RoundSince + newAction.TotalStaked = &staked + newAction.TotalRecovered = &recovered + } + } + return nil + }, +} + +func hipoTreasuryExternal(op abi.MsgOpName) []bubbleCheck { + return []bubbleCheck{IsTx, IsAccount(references.HipoTreasury), HasOperation(op), IsExternal} +} + +func hipoRoundBuilder(transition PoolRoundTransitionType) func(*BubblePoolRoundTransition, *Bubble) error { + return func(newAction *BubblePoolRoundTransition, bubble *Bubble) error { + tx := bubble.Info.(BubbleTx) + newAction.Pool = tx.account.Address + newAction.Transition = transition + newAction.Implementation = core.StakingImplementationHipo + newAction.Success = tx.success + switch body := tx.decodedBody.Value.(type) { + case abi.HipoFinanceParticipateInElectionExtInMsgBody: + newAction.RoundSince = body.RoundSince + case abi.HipoFinanceVsetChangedExtInMsgBody: + newAction.RoundSince = body.RoundSince + case abi.HipoFinanceFinishParticipationExtInMsgBody: + newAction.RoundSince = body.RoundSince + } + return nil + } +} + +func hipoRoundIs(transitions ...PoolRoundTransitionType) bubbleCheck { + return func(bubble *Bubble) bool { + round, ok := bubble.Info.(BubblePoolRoundTransition) + if !ok || round.Implementation != core.StakingImplementationHipo { + return false + } + return slices.Contains(transitions, round.Transition) + } +} + +func hipoKeepRound(newAction *BubblePoolRoundTransition, bubble *Bubble) error { + *newAction = bubble.Info.(BubblePoolRoundTransition) + // the transfers folded in next must not write into the kept round's backing array + newAction.transfers = slices.Clip(newAction.transfers) + return nil +} + +func hipoOptionalLastBill() *Straw[BubblePoolRoundTransition] { + s := Optional(hipoLastBillBurned) + return &s +} + +func hipoIssueLog(tx BubbleTx) (core.HipoLoanLog, tongo.AccountID, bool) { + // The treasury writes the loan's log::loan straight after sending its proxy_new_stake, in + // the same transaction. The log names the borrower and the amounts; the message names + // neither. + if tx.inputFrom == nil || tx.inputFrom.Address != references.HipoTreasury { + return core.HipoLoanLog{}, tongo.AccountID{}, false + } + for _, msg := range tx.senderExternalOut { + if msg.CreatedLt != tx.inputLt+1 { + continue + } + topic, value, ok := core.DecodeHipoLog(msg) + if !ok || topic != core.HipoLogLoan { + continue + } + log, ok := value.(core.HipoLoanLog) + if !ok { + return core.HipoLoanLog{}, tongo.AccountID{}, false + } + borrower, ok := hipoOwner(log.Borrower) + return log, borrower, ok + } + return core.HipoLoanLog{}, tongo.AccountID{}, false +} + +func hipoLogs(bubble *Bubble) map[core.HipoLogTopic][]any { + // Only the treasury's own transaction is read: the topic is an address, so any contract + // may emit a message that looks like one of these, and a log is only Hipo's if Hipo's + // treasury sent it. + tx, ok := bubble.Info.(BubbleTx) + if !ok || tx.account.Address != references.HipoTreasury { + return nil + } + var found map[core.HipoLogTopic][]any + for _, msg := range tx.externalOut { + topic, value, ok := core.DecodeHipoLog(msg) + if !ok { + continue + } + if found == nil { + found = make(map[core.HipoLogTopic][]any, 2) + } + found[topic] = append(found[topic], value) + } + return found +} + +func hipoRepaymentLog(logs map[core.HipoLogTopic][]any) (core.HipoRepaymentLog, tongo.AccountID, bool) { + repayments := logs[core.HipoLogRepayment] + if len(repayments) != 1 { + return core.HipoRepaymentLog{}, tongo.AccountID{}, false + } + log, ok := repayments[0].(core.HipoRepaymentLog) + if !ok { + return core.HipoRepaymentLog{}, tongo.AccountID{}, false + } + borrower, ok := hipoOwner(log.Borrower) + return log, borrower, ok +} + +func hipoVarCoins(c tlb.VarUInteger16) *int64 { + // A request may claim any 120-bit number; one past int64 is reported as unknown rather + // than wrapped, and the treasury refuses it anyway. + v := big.Int(c) + if !v.IsInt64() { + return nil + } + n := v.Int64() + return &n +} + +func addTransfer[T any, P interface { + *T + add(*Bubble) +}](newAction P, bubble *Bubble) error { + newAction.add(bubble) + return nil +} diff --git a/pkg/bath/hipo_loans_test.go b/pkg/bath/hipo_loans_test.go new file mode 100644 index 000000000..8f4f308ae --- /dev/null +++ b/pkg/bath/hipo_loans_test.go @@ -0,0 +1,288 @@ +package bath + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/abi" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" +) + +// Real logs, the same ones pkg/core's parser tests read. +const ( + hipoLoanLogBody = "b5ee9c720101010100460000876a9e4f0850ba43b740007077038d7ea4c68000708f0db1157feef523aff0d8e07fff801a6d1cefedb5ca6bcc88f0a7300745ee3e0fbaf722aa9040af454ee47dc2d130d0" + hipoRepaymentLogBody = "b5ee9c7201010101004b0000916a944f08707268034320633703328b944c4000703f288f2fa449a601375194ce1c05344a2f57fd8007b591d71001a927c6fc6b00e107ffea04e4a60f955f4f6c6475b5d6c02703bcb0" + hipoFinishLogBody = "b5ee9c720101010100240000446a944f0870e4a290e8d093470e4c97b1b6a56c71c940a9e703cb07189eaf16dfb68a" + // when each was written: the loan after the 2026-09-05 release, the repayment before it + hipoLoanLogAt = 1788761000 + hipoRepaymentLogAt = 1788200000 +) + +func TestHipoLoanStraws(t *testing.T) { + loan := tongo.MustParseAccountID("-1:1e530983ee3f8be8529cc6c03199953f9fe9b46f7845219f78e888968e9c54ea") + t.Run("a loan is issued from the log written next to its message", func(t *testing.T) { + issue := hipoSent(hipoFrom(hipoTx(loan, abi.HipoFinanceProxyNewStakeMsgOp), references.HipoTreasury), 100, + hipoLogMsg(t, core.HipoLogLoan, 99, hipoLoanLogBody), + hipoLogMsg(t, core.HipoLogLoan, 101, hipoLoanLogBody), + ) + require.True(t, LoanIssueHipoStraw.Merge(issue)) + issued := issue.Info.(BubbleLoanIssue) + want := hipoDecodedLog(t, core.HipoLogLoan, hipoLoanLogBody).(core.HipoLoanLog) + require.Equal(t, int64(want.LoanAmount), issued.Amount) + require.Equal(t, loan, issued.Loan) + require.Equal(t, references.HipoTreasury, issued.Pool) + require.Contains(t, issue.Accounts, issued.Borrower, "the borrower sees the loan in their history") + }) + t.Run("a loan with no log of its own is not issued", func(t *testing.T) { + issue := hipoSent(hipoFrom(hipoTx(loan, abi.HipoFinanceProxyNewStakeMsgOp), references.HipoTreasury), 100, + hipoLogMsg(t, core.HipoLogLoan, 99, hipoLoanLogBody), + ) + require.False(t, LoanIssueHipoStraw.Merge(issue)) + }) + t.Run("a repayment is split by its log", func(t *testing.T) { + result := hipoLogged(hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceRecoverStakeResultMsgOp, + hipoFrom(hipoTx(loan, abi.HipoFinanceLoanResultMsgOp), references.HipoTreasury), + ), loan), hipoLogMsg(t, core.HipoLogRepayment, 1, hipoRepaymentLogBody)) + require.True(t, LoanRepaymentHipoStraw.Merge(result)) + repaid := result.Info.(BubbleLoanRepayment) + want := hipoDecodedLog(t, core.HipoLogRepayment, hipoRepaymentLogBody).(core.HipoRepaymentLog) + require.Equal(t, int64(want.StakersShare), repaid.PoolReward) + require.Equal(t, int64(want.BorrowerShare), repaid.BorrowerPayout) + require.Equal(t, loan, repaid.Loan) + require.Empty(t, result.Children, "loan_result is part of the repayment") + }) + t.Run("the settled round carries the round's totals", func(t *testing.T) { + collection := tongo.MustParseAccountID("0:7e40f97d00000000000000000000000000000000000000000000000000000000") + burn := hipoFrom(hipoTx(collection, abi.HipoFinanceBurnAllMsgOp, + hipoLogged(hipoTx(references.HipoTreasury, abi.HipoFinanceLastBillBurnedMsgOp), + hipoLogMsg(t, core.HipoLogFinish, 1, hipoFinishLogBody)), + ), references.HipoTreasury) + require.True(t, PoolRoundSettledHipoStraw.Merge(burn)) + settled := burn.Info.(BubblePoolRoundTransition) + want := hipoDecodedLog(t, core.HipoLogFinish, hipoFinishLogBody).(core.HipoFinishLog) + require.Equal(t, PoolRoundSettled, settled.Transition) + require.Equal(t, want.RoundSince, settled.RoundSince) + require.Equal(t, int64(want.TotalRecovered), *settled.TotalRecovered) + }) + t.Run("the round folds in the treasury's own steps", func(t *testing.T) { + round := hipoExternal(hipoTx(references.HipoTreasury, abi.HipoFinanceParticipateInElectionExtInMsgOp, + hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceDecideLoanRequestsMsgOp, + hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceProcessLoanRequestsMsgOp, + hipoFrom(hipoTx(loan, abi.HipoFinanceProxyNewStakeMsgOp), references.HipoTreasury), + ), references.HipoTreasury), + ), references.HipoTreasury), + ), abi.HipoFinanceParticipateInElectionExtInMsgBody{RoundSince: 1792102152}) + MergeAllBubbles(round, []Merger{PoolRoundElectionHipoStraw, hipoRoundElectionStepStraw}) + election := round.Info.(BubblePoolRoundTransition) + require.Equal(t, PoolRoundElection, election.Transition) + require.Equal(t, uint32(1792102152), election.RoundSince) + require.Len(t, round.Children, 1, "only the loan is left beside the round") + }) + t.Run("a failed step is not folded into the round", func(t *testing.T) { + failed := hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceProcessLoanRequestsMsgOp), references.HipoTreasury) + tx := failed.Info.(BubbleTx) + tx.success = false + failed.Info = tx + round := hipoExternal(hipoTx(references.HipoTreasury, abi.HipoFinanceParticipateInElectionExtInMsgOp, + hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceDecideLoanRequestsMsgOp, failed), references.HipoTreasury), + ), abi.HipoFinanceParticipateInElectionExtInMsgBody{RoundSince: 1792102152}) + MergeAllBubbles(round, []Merger{PoolRoundElectionHipoStraw, hipoRoundElectionStepStraw}) + require.True(t, round.Info.(BubblePoolRoundTransition).Success) + require.Len(t, round.Children, 1, "the failed step stays beside the round") + require.Equal(t, failed, round.Children[0]) + }) + t.Run("a vset change with an unread body is not taken for the old round", func(t *testing.T) { + round := hipoExternal(hipoTx(references.HipoTreasury, abi.HipoFinanceVsetChangedExtInMsgOp), nil) + tx := round.Info.(BubbleTx) + tx.utime = 1792102152 + round.Info = tx + require.True(t, PoolRoundBeganHipoStraw.Merge(round)) + require.Equal(t, PoolRoundBegan, round.Info.(BubblePoolRoundTransition).Transition) + }) +} + +func TestHipoLoanStrawsRejectForgeries(t *testing.T) { + // Every loan action names a borrower and an amount, so each is anchored on something only + // the treasury can produce: its own logs, or its own transaction. + attacker := tongo.MustParseAccountID("0:1111111111111111111111111111111111111111111111111111111111111111") + t.Run("a log next to a message the treasury did not send", func(t *testing.T) { + forged := hipoSent(hipoFrom(hipoTx(attacker, abi.HipoFinanceProxyNewStakeMsgOp), attacker), 100, + hipoLogMsg(t, core.HipoLogLoan, 101, hipoLoanLogBody)) + require.False(t, LoanIssueHipoStraw.Merge(forged)) + }) + t.Run("a repayment log from another contract", func(t *testing.T) { + forged := hipoLogged(hipoFrom(hipoTx(attacker, abi.HipoFinanceRecoverStakeResultMsgOp), attacker), + hipoLogMsg(t, core.HipoLogRepayment, 1, hipoRepaymentLogBody)) + require.False(t, LoanRepaymentHipoStraw.Merge(forged)) + }) + t.Run("a recover_stake_result the treasury did not log", func(t *testing.T) { + forged := hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceRecoverStakeResultMsgOp), attacker) + require.False(t, LoanRepaymentHipoStraw.Merge(forged)) + }) + t.Run("a round trigger at another contract", func(t *testing.T) { + forged := hipoExternal(hipoTx(attacker, abi.HipoFinanceFinishParticipationExtInMsgOp), + abi.HipoFinanceFinishParticipationExtInMsgBody{}) + require.False(t, PoolRoundEndedHipoStraw.Merge(forged)) + }) + t.Run("a burn_all the treasury did not send", func(t *testing.T) { + forged := hipoFrom(hipoTx(attacker, abi.HipoFinanceBurnAllMsgOp), attacker) + require.False(t, PoolRoundSettledHipoStraw.Merge(forged)) + }) + t.Run("a loan request at another contract", func(t *testing.T) { + forged := hipoFrom(hipoTx(attacker, abi.HipoFinanceRequestLoanV4MsgOp), attacker) + require.False(t, LoanRequestHipoStraw.Merge(forged)) + }) +} + +func TestHipoLoanActionsAccountForTheirTransfers(t *testing.T) { + loan := tongo.MustParseAccountID("-1:1e530983ee3f8be8529cc6c03199953f9fe9b46f7845219f78e888968e9c54ea") + borrower := tongo.MustParseAccountID("0:2222222222222222222222222222222222222222222222222222222222222222") + governor := tongo.MustParseAccountID("0:3333333333333333333333333333333333333333333333333333333333333333") + collection := tongo.MustParseAccountID("0:4444444444444444444444444444444444444444444444444444444444444444") + t.Run("a repayment counts every leg it absorbs", func(t *testing.T) { + result := hipoLogged(hipoPaid(hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceRecoverStakeResultMsgOp, + hipoPaid(hipoFrom(hipoTx(borrower, abi.HipoFinanceLoanResultMsgOp), references.HipoTreasury), 30), + hipoPaid(hipoFrom(hipoTx(references.HipoBurner, abi.HipoFinanceTakeBorrowerFeeMsgOp), references.HipoTreasury), 5), + hipoPaid(hipoFrom(hipoTx(governor, abi.HipoFinanceTakeProfitMsgOp), references.HipoTreasury), 7), + ), loan), 1000), hipoLogMsg(t, core.HipoLogRepayment, 1, hipoRepaymentLogBody)) + require.True(t, LoanRepaymentHipoStraw.Merge(result)) + action := result.Info.(BubbleLoanRepayment).ToAction() + require.Equal(t, int64(-1000), action.ContributeToExtra(loan)) + require.Equal(t, int64(1000-30-5-7), action.ContributeToExtra(references.HipoTreasury)) + require.Equal(t, int64(30), action.ContributeToExtra(borrower)) + require.Equal(t, int64(5), action.ContributeToExtra(references.HipoBurner)) + require.Equal(t, int64(7), action.ContributeToExtra(governor)) + }) + t.Run("a refused request counts its refund", func(t *testing.T) { + request := hipoPaid(hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceRequestLoanV4MsgOp, + hipoBounced(hipoPaid(hipoFrom(hipoTx(borrower, abi.HipoFinanceRequestLoanV4MsgOp), references.HipoTreasury), 999)), + ), borrower), 1000) + require.True(t, LoanRequestHipoStraw.Merge(request)) + action := request.Info.(BubbleLoanRequest).ToAction() + require.Equal(t, int64(-1), action.ContributeToExtra(borrower)) + require.Equal(t, int64(1), action.ContributeToExtra(references.HipoTreasury)) + }) + t.Run("a round counts the stakes it asks back and the bills it burns", func(t *testing.T) { + round := hipoExternal(hipoTx(references.HipoTreasury, abi.HipoFinanceFinishParticipationExtInMsgOp, + hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceRecoverStakesMsgOp, + hipoPaid(hipoFrom(hipoTx(loan, abi.HipoFinanceProxyRecoverStakeMsgOp), references.HipoTreasury), 2), + ), references.HipoTreasury), + ), abi.HipoFinanceFinishParticipationExtInMsgBody{RoundSince: 1792102152}) + MergeAllBubbles(round, []Merger{PoolRoundEndedHipoStraw, hipoRoundEndedStepStraw, hipoRoundRecoverStraw}) + action := round.Info.(BubblePoolRoundTransition).ToAction() + require.Equal(t, int64(-2), action.ContributeToExtra(references.HipoTreasury)) + require.Equal(t, int64(2), action.ContributeToExtra(loan)) + burn := hipoPaid(hipoFrom(hipoTx(collection, abi.HipoFinanceBurnAllMsgOp, + hipoPaid(hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceLastBillBurnedMsgOp), collection), 3), + ), references.HipoTreasury), 4) + require.True(t, PoolRoundSettledHipoStraw.Merge(burn)) + action = burn.Info.(BubblePoolRoundTransition).ToAction() + require.Equal(t, int64(-4+3), action.ContributeToExtra(references.HipoTreasury)) + require.Equal(t, int64(4-3), action.ContributeToExtra(collection)) + }) +} + +func TestHipoLoanStrawsSkipUnreadableLogs(t *testing.T) { + loan := tongo.MustParseAccountID("-1:1e530983ee3f8be8529cc6c03199953f9fe9b46f7845219f78e888968e9c54ea") + t.Run("a repayment log that does not fit its layout", func(t *testing.T) { + log := hipoLogMsg(t, core.HipoLogRepayment, 1, hipoRepaymentLogBody) + log.CreatedAt = 0 + result := hipoLogged(hipoFrom(hipoTx(references.HipoTreasury, abi.HipoFinanceRecoverStakeResultMsgOp), loan), log) + require.False(t, LoanRepaymentHipoStraw.Merge(result)) + }) + t.Run("a loan log that names no borrower", func(t *testing.T) { + cell := boc.NewCell() + require.NoError(t, tlb.Marshal(cell, core.HipoLoanLog{RoundSince: 1, Borrower: tlb.MsgAddress{SumType: "AddrNone"}})) + body, err := cell.ToBoc() + require.NoError(t, err) + log := hipoLogMsg(t, core.HipoLogLoan, 101, hipoLoanLogBody) + log.Body = body + issue := hipoSent(hipoFrom(hipoTx(loan, abi.HipoFinanceProxyNewStakeMsgOp), references.HipoTreasury), 100, log) + require.False(t, LoanIssueHipoStraw.Merge(issue)) + }) +} + +func TestHipoSettledRoundIsNamedByTheLastRepayment(t *testing.T) { + collection := tongo.MustParseAccountID("0:4444444444444444444444444444444444444444444444444444444444444444") + burn := hipoSent(hipoFrom(hipoTx(collection, abi.HipoFinanceBurnAllMsgOp), references.HipoTreasury), 10, + hipoLogMsg(t, core.HipoLogRepayment, 11, hipoRepaymentLogBody)) + require.True(t, PoolRoundSettledHipoStraw.Merge(burn)) + want := hipoDecodedLog(t, core.HipoLogRepayment, hipoRepaymentLogBody).(core.HipoRepaymentLog) + require.Equal(t, want.RoundSince, burn.Info.(BubblePoolRoundTransition).RoundSince) +} + +func hipoLogMsg(t *testing.T, topic core.HipoLogTopic, lt uint64, body string) core.Message { + // built the way emit_log does: the topic in a 256-bit external destination + at := uint32(hipoLoanLogAt) + if topic == core.HipoLogRepayment { + at = hipoRepaymentLogAt + } + t.Helper() + dest := boc.NewBitString(256) + require.NoError(t, dest.WriteBigUint(new(big.Int).SetUint64(uint64(topic)), 256)) + return core.Message{ + MessageID: core.MessageID{CreatedLt: lt}, + CreatedAt: at, + MsgType: core.ExtOutMsg, + DestinationExtern: &dest, + Body: mustHex(t, body), + } +} + +func hipoDecodedLog(t *testing.T, topic core.HipoLogTopic, body string) any { + t.Helper() + decoded, value, ok := core.DecodeHipoLog(hipoLogMsg(t, topic, 0, body)) + require.True(t, ok) + require.Equal(t, topic, decoded) + return value +} + +func hipoLogged(b *Bubble, logs ...core.Message) *Bubble { + tx := b.Info.(BubbleTx) + tx.externalOut = logs + b.Info = tx + return b +} + +func hipoSent(b *Bubble, lt uint64, logs ...core.Message) *Bubble { + tx := b.Info.(BubbleTx) + tx.inputLt = lt + tx.senderExternalOut = logs + b.Info = tx + return b +} + +func hipoExternal(b *Bubble, body any) *Bubble { + tx := b.Info.(BubbleTx) + tx.external = true + tx.decodedBody.Value = body + b.Info = tx + return b +} + +func hipoPaid(b *Bubble, amount int64) *Bubble { + tx := b.Info.(BubbleTx) + tx.inputAmount = amount + b.Info = tx + return b +} + +func hipoBounced(b *Bubble) *Bubble { + tx := b.Info.(BubbleTx) + tx.bounced = true + b.Info = tx + return b +} + +func mustHex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s) + require.NoError(t, err) + return b +} diff --git a/pkg/bath/hipo_owner_test.go b/pkg/bath/hipo_owner_test.go index ad5db2400..3e30dac79 100644 --- a/pkg/bath/hipo_owner_test.go +++ b/pkg/bath/hipo_owner_test.go @@ -243,13 +243,14 @@ func TestHipoOwnerSeesTheirOwnFlow(t *testing.T) { { name: "unstake-all by comment, paid now", trace: hipoUnstakeAll(hipoPayout()), - want: []string{"JettonBurn", "WithdrawStake"}, + // the request names no amount, the burn and the payout below it do + want: []string{"WithdrawStakeRequest", "JettonBurn", "WithdrawStake"}, hgram: -hipoTestTokens, }, { name: "unstake-all by comment, deferred", trace: hipoUnstakeAll(hipoBill(references.HipoTreasury, 0)), - want: []string{"JettonBurn", "WithdrawStakeRequest"}, + want: []string{"WithdrawStakeRequest", "JettonBurn", "WithdrawStakeRequest"}, hgram: -hipoTestTokens, }, { diff --git a/pkg/bath/loans.go b/pkg/bath/loans.go new file mode 100644 index 000000000..0184d00fd --- /dev/null +++ b/pkg/bath/loans.go @@ -0,0 +1,162 @@ +package bath + +import ( + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/tongo" +) + +// A lending pool lends its stakers' coins to validators, one validation round at a time. These +// actions show that side of a pool: a validator asks for a loan, the pool lends and the loan is +// staked, the elector returns it with the round's reward and the pool splits that reward, and the +// round moves through its stages. They are generic, like DepositStake, and say which pool +// implementation produced them. + +const ( + // PoolRoundElection: the pool decided which loans to make and staked them with the elector. + PoolRoundElection PoolRoundTransitionType = "election" + // PoolRoundBegan: the validation round started. + PoolRoundBegan PoolRoundTransitionType = "began" + // PoolRoundValidated: the round's validators finished validating, and the stakes are held + // for a while before the elector releases them. + PoolRoundValidated PoolRoundTransitionType = "validated" + // PoolRoundEnded: the round's stakes were released and the pool is recovering them. + PoolRoundEnded PoolRoundTransitionType = "ended" + // PoolRoundSettled: every loan was recovered and the round's deposits and withdrawals + // were settled. + PoolRoundSettled PoolRoundTransitionType = "settled" +) + +type PoolRoundTransitionType string + +type ( + LoanRequestAction struct { + Borrower tongo.AccountID + Pool tongo.AccountID + RoundSince uint32 + Amount *int64 + MinPayment *int64 + TonAttached int64 + Implementation core.StakingImplementation + transfers + } + LoanIssueAction struct { + Borrower tongo.AccountID + Pool tongo.AccountID + Loan tongo.AccountID + RoundSince uint32 + Amount int64 + Accrued int64 + Collateral int64 + MinPayment int64 + Implementation core.StakingImplementation + transfers + } + LoanRepaymentAction struct { + Borrower tongo.AccountID + Pool tongo.AccountID + Loan tongo.AccountID + RoundSince uint32 + Amount int64 + Accrued int64 + Recovered int64 + PoolReward int64 + ProtocolFee int64 + BorrowerPayout int64 + BurnAmount int64 + Shortfall *int64 + Implementation core.StakingImplementation + transfers + } + PoolRoundTransitionAction struct { + Pool tongo.AccountID + RoundSince uint32 + Transition PoolRoundTransitionType + TotalStaked *int64 + TotalRecovered *int64 + Implementation core.StakingImplementation + transfers + } +) + +type BubbleLoanRequest struct { + LoanRequestAction + Success bool +} + +type BubbleLoanIssue struct { + LoanIssueAction + Success bool +} + +type BubbleLoanRepayment struct { + LoanRepaymentAction + Success bool +} + +type BubblePoolRoundTransition struct { + PoolRoundTransitionAction + Success bool +} + +type ( + // transfers are the coins carried by the messages an action absorbed, so that they count + // towards the action rather than as an account's unexplained extra + transfers []transfer + transfer struct { + from tongo.AccountID + to tongo.AccountID + amount int64 + } +) + +func (a *LoanRequestAction) SubjectAccounts() []tongo.AccountID { + return []tongo.AccountID{a.Borrower, a.Pool} +} + +func (a *LoanIssueAction) SubjectAccounts() []tongo.AccountID { + return []tongo.AccountID{a.Borrower, a.Pool, a.Loan} +} + +func (a *LoanRepaymentAction) SubjectAccounts() []tongo.AccountID { + return []tongo.AccountID{a.Borrower, a.Pool, a.Loan} +} + +func (a *PoolRoundTransitionAction) SubjectAccounts() []tongo.AccountID { + return []tongo.AccountID{a.Pool} +} + +func (b BubbleLoanRequest) ToAction() *Action { + a := b.LoanRequestAction + return &Action{LoanRequest: &a, Success: b.Success, Type: LoanRequest} +} + +func (b BubbleLoanIssue) ToAction() *Action { + a := b.LoanIssueAction + return &Action{LoanIssue: &a, Success: b.Success, Type: LoanIssue} +} + +func (b BubbleLoanRepayment) ToAction() *Action { + a := b.LoanRepaymentAction + return &Action{LoanRepayment: &a, Success: b.Success, Type: LoanRepayment} +} + +func (b BubblePoolRoundTransition) ToAction() *Action { + a := b.PoolRoundTransitionAction + return &Action{PoolRoundTransition: &a, Success: b.Success, Type: PoolRoundTransition} +} + +func (t *transfers) add(bubble *Bubble) { + tx := bubble.Info.(BubbleTx) + if tx.inputFrom == nil { + return + } + *t = append(*t, transfer{from: tx.inputFrom.Address, to: tx.account.Address, amount: tx.inputAmount}) +} + +func (t transfers) contributeToExtra(account tongo.AccountID) int64 { + var extra int64 + for _, tr := range t { + extra += detectDirection(account, tr.from, tr.to, tr.amount) + } + return extra +} diff --git a/pkg/bath/matcher.go b/pkg/bath/matcher.go index 9ffa017a9..c239f88d8 100644 --- a/pkg/bath/matcher.go +++ b/pkg/bath/matcher.go @@ -1,6 +1,7 @@ package bath import ( + "slices" "unsafe" "github.com/tonkeeper/opentonapi/pkg/sentry" @@ -85,12 +86,11 @@ func (s Straw[newBubbleT]) Merge(bubble *Bubble) bool { var newBubble newBubbleT var newChildren []*Bubble var potentialChildren []*Bubble + // The head's accounts and transactions are added with the rest of the mapping below, after + // its builder has run. Starting from them as well would double them each time a straw + // extends an action it built before. var newAccounts []tongo.AccountID var newTransaction []ton.Bits256 - if !s.NotMergeBubble { - newAccounts = bubble.Accounts - newTransaction = bubble.Transaction - } nvf := newValueFlow() var finalizer func(newAction *newBubbleT, flow *ValueFlow) merged := make(map[*Bubble]struct{}, len(mapping)) @@ -177,6 +177,11 @@ func IsTx(b *Bubble) bool { return ok } +func IsSuccess(b *Bubble) bool { + tx, ok := b.Info.(BubbleTx) + return ok && tx.success +} + type interfaceType struct { typ unsafe.Pointer word unsafe.Pointer @@ -254,6 +259,13 @@ func HasOperation(name abi.MsgOpName) bubbleCheck { return b.Info.(BubbleTx).operation(name) } } + +func HasAnyOperation(names ...abi.MsgOpName) bubbleCheck { + return func(b *Bubble) bool { + return slices.ContainsFunc(names, b.Info.(BubbleTx).operation) + } +} + func IsAccount(id tongo.AccountID) bubbleCheck { return func(bubble *Bubble) bool { return bubble.Info.(BubbleTx).account.Address == id diff --git a/pkg/bath/straws.go b/pkg/bath/straws.go index fcadbfb93..72ec172f4 100644 --- a/pkg/bath/straws.go +++ b/pkg/bath/straws.go @@ -60,86 +60,104 @@ func DefaultStraws(book AddressBook, infoSource core.InformationSource) []Merger // Before JettonBurnStraw, which names no owner for it without additional info. JettonBurnHipoUnstakeAllStraw, JettonBurnStraw, + // 15 WtonMintStraw, NftPurchaseStraw, - // 15 StonfiSwapStraw, UniversalStonfiStraw{}, UniversalDedustStraw{}, + // 20 TgAuctionV1InitialBidStraw, StrawAuctionBigGetgems, - // 20 StrawAuctionBuyGetgems, StrawAuctionBuyFragments, JettonMintFromMasterStraw, + // 25 JettonMintStrawGovernance, InvoicePaymentStrawNative, - // 25 InvoicePaymentStrawJetton, MegatonFiJettonSwap, UnSubscriptionBySubscriberStraw, + // 30 UnSubscriptionByBeneficiaryOrExpiredStraw, SubscriptionDeployStraw, - // 30 SubscriptionPaymentStraw, SubscriptionPaymentWithRequestFundsStraw, DepositLiquidStakeStraw, + // 35 PendingWithdrawRequestLiquidStraw, ElectionsDepositStakeStraw, - // 35 ElectionsRecoverStakeStraw, DepositTFStakeStraw, WithdrawTFStakeRequestStraw, + // 40 WithdrawStakeImmediatelyStraw, WithdrawLiquidStake, - // 40 DNSRenewStraw, BidaskLiquidityDepositBothNativeStraw, BidaskLiquidityDepositBothJettonStraw, + // 45 BidaskLiquidityDepositJettonStraw, StonfiLiquidityDepositSingle, - // 45 StonfiLiquidityDepositBoth, DepositEthenaStakeStraw, WithdrawEthenaStakeRequestStraw, + // 50 BidaskSwapStraw, BidaskSwapStrawReverse, - // 50 BidaskJettonSwapStraw, MooncxSwapStraw, MooncxSwapStrawReverse, + // 55 MoocxLiquidityDepositJettonStraw, MoocxLiquidityDepositNativeStraw, - // 55 MoocxLiquidityDepositBothStraw, ToncoSwapStraw, ToncoDepositLiquiditySingleStraw, + // 60 ToncoDepositLiquidityBothStraw, ToncoDepositLiquidityWithRefundStraw, - // 60 DepositAffluentEarnStraw, DepositAffluentEarnWithOraclesStraw, WithdrawAffluentEarnRequestStraw, + // 65 InstantWithdrawAffluentEarnStraw, InstantWithdrawAffluentEarnWithOraclesStraw, - // 65 PythOraclePriceUpdateStraw(infoSource), DepositFFVaultStakeStraw, WithdrawalRequestFFVaultStraw, + // 70 XTRDepositAction, XTRBuyAction, - // 70 // Hipo, in this order: the deposit straws stop above the mint, and // WithdrawHipoStakeStraw upgrades the request. JettonMintHipoStraw, DepositHipoStakeStraw, DepositHipoStakeDeferredStraw, + // 75 WithdrawHipoStakeRequestStraw, WithdrawHipoStakeStraw, - // 75 WithdrawHipoStakeSettledStraw, WithdrawHipoStakePostponedStraw, JettonMintHipoRollbackStraw, + // 80 + WithdrawHipoStakeAllRequestStraw, + // Hipo's loans and rounds, after the staking straws: a settled round folds in the + // bills' burning but leaves their settlements to the straws above. + LoanRequestHipoStraw, + LoanIssueHipoStraw, + LoanRepaymentHipoStraw, + PoolRoundElectionHipoStraw, + // 85 + PoolRoundBeganHipoStraw, + PoolRoundEndedHipoStraw, + PoolRoundSettledHipoStraw, + hipoRoundElectionStepStraw, + hipoRoundEndedStepStraw, + // 90 + hipoRoundRecoverStraw, + hipoRoundBillStraw, + hipoRoundMintStraw, } } diff --git a/pkg/bath/testdata/hipo-loan-request.json b/pkg/bath/testdata/hipo-loan-request.json new file mode 100644 index 000000000..8f52b203c --- /dev/null +++ b/pkg/bath/testdata/hipo-loan-request.json @@ -0,0 +1,34 @@ +{ + "Actions": [ + { + "LoanRequest": { + "Borrower": "0:9c9b4ce7de3773c2a5f905f6f72c90392f0d79e149e8886df0aaa95bbe2b0fff", + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "RoundSince": 1792102152, + "Amount": 1925374834094334, + "MinPayment": 1219770712064, + "TonAttached": 1322568227845, + "Implementation": "hipo" + }, + "Success": true, + "Type": "LoanRequest", + "BaseTransactions": [ + "9f04f4bab2f587c110da66dbee299f33a185f3041bcc3c2b350474d71de2dce3" + ] + } + ], + "Accounts": [ + { + "Account": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Ton": 1322566434388, + "Fee": 1793457, + "Jettons": null + }, + { + "Account": "0:9c9b4ce7de3773c2a5f905f6f72c90392f0d79e149e8886df0aaa95bbe2b0fff", + "Ton": -1322568811481, + "Fee": 583636, + "Jettons": null + } + ] + } \ No newline at end of file diff --git a/pkg/bath/testdata/hipo-round-began.json b/pkg/bath/testdata/hipo-round-began.json new file mode 100644 index 000000000..4d7090c1a --- /dev/null +++ b/pkg/bath/testdata/hipo-round-began.json @@ -0,0 +1,27 @@ +{ + "Actions": [ + { + "PoolRoundTransition": { + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "RoundSince": 1792102152, + "Transition": "began", + "TotalStaked": null, + "TotalRecovered": null, + "Implementation": "hipo" + }, + "Success": true, + "Type": "PoolRoundTransition", + "BaseTransactions": [ + "87a89f582a6d372beece6a29aa5d306240065fdfe00897263fd5925d0e248e60" + ] + } + ], + "Accounts": [ + { + "Account": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Ton": -618761, + "Fee": 618761, + "Jettons": null + } + ] + } \ No newline at end of file diff --git a/pkg/bath/testdata/hipo-round-election.json b/pkg/bath/testdata/hipo-round-election.json new file mode 100644 index 000000000..40514e813 --- /dev/null +++ b/pkg/bath/testdata/hipo-round-election.json @@ -0,0 +1,72 @@ +{ + "Actions": [ + { + "PoolRoundTransition": { + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "RoundSince": 1788759816, + "Transition": "election", + "TotalStaked": null, + "TotalRecovered": null, + "Implementation": "hipo" + }, + "Success": true, + "Type": "PoolRoundTransition", + "BaseTransactions": [ + "046a4f3214343224ea7406f15767ef4fdbb50ec75e7b8976fc9f0bcbba093a90", + "42b2852b100ba5a8de60fae82c50cb5f7b13fafdeee5926def6355ade9b137b0", + "93a7eb39d4e5efe539c99484c073d24ceecd61d69f6e623c8673e00831e2356c" + ] + }, + { + "LoanIssue": { + "Borrower": "0:d368e77f6dae535e64478539803a2f71f07dd7b9155482057a2a7723ee168986", + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Loan": "-1:d608dbd87ebf6ad63b1fe270d97613f11300eff4e36ff7e4ab96e838fdf92694", + "RoundSince": 1788759816, + "Amount": 1000000000000000, + "Accrued": 2516623493168879, + "Collateral": 153275652320, + "MinPayment": 50000000000, + "Implementation": "hipo" + }, + "Success": true, + "Type": "LoanIssue", + "BaseTransactions": [ + "6da5b3396e705cf241543ba3aade78b287ddb06ddaeefaabd9dbe565db7e9ed2" + ] + }, + { + "ElectionsDepositStake": { + "Amount": 3516777095561199, + "Elector": "-1:3333333333333333333333333333333333333333333333333333333333333333", + "Staker": "-1:d608dbd87ebf6ad63b1fe270d97613f11300eff4e36ff7e4ab96e838fdf92694" + }, + "Success": true, + "Type": "ElectionsDepositStake", + "BaseTransactions": [ + "30a14fb8fa189e5741dde534d3c0d410f855afa814df632e0d442585537ee46f", + "6adb33d627660ff1871ee1dea9a0222472e92f85398a4b71291f063a241af235" + ] + } + ], + "Accounts": [ + { + "Account": "-1:3333333333333333333333333333333333333333333333333333333333333333", + "Ton": 3516776095561199, + "Fee": 0, + "Jettons": null + }, + { + "Account": "-1:d608dbd87ebf6ad63b1fe270d97613f11300eff4e36ff7e4ab96e838fdf92694", + "Ton": 983080000, + "Fee": 107360000, + "Jettons": null + }, + { + "Account": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Ton": -3516777223992553, + "Fee": 37991354, + "Jettons": null + } + ] + } \ No newline at end of file diff --git a/pkg/bath/testdata/hipo-round-end.json b/pkg/bath/testdata/hipo-round-end.json new file mode 100644 index 000000000..93475988a --- /dev/null +++ b/pkg/bath/testdata/hipo-round-end.json @@ -0,0 +1,464 @@ +{ + "Actions": [ + { + "PoolRoundTransition": { + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "RoundSince": 1791971080, + "Transition": "ended", + "TotalStaked": null, + "TotalRecovered": null, + "Implementation": "hipo" + }, + "Success": true, + "Type": "PoolRoundTransition", + "BaseTransactions": [ + "3239221a9ea152065260d6410eec8cda9ea8eb852f664f40d116ff79c6ee9c2f", + "a0b6b9fdedceee19929e1247c72e4b34e40ee6647fa193e9f4d3e2281ab4ce63", + "ac57cbe3d868263d99ca2f2e398129b65267d8d0d2f97ed889627c672088518a", + "d8d505bceb48b1916f7211faa604168d42f10932ab939f831515c84a2b5d2de6" + ] + }, + { + "ElectionsRecoverStake": { + "Amount": 2039594349848138, + "Elector": "-1:3333333333333333333333333333333333333333333333333333333333333333", + "Staker": "-1:ee9ab316f8ea953833267a1efc9bf973bc9346d02781f2c6ae2601eb59923ea7" + }, + "Success": true, + "Type": "ElectionsRecoverStake", + "BaseTransactions": [ + "2912e6201746e4157298b59a527126c126245d18dfb664cd7d8a5ec27922edfe", + "bd8a6b2bce5d27cb2e40551ace7efecd47062ec7b6cd4d371951c2fc90bf6e90" + ] + }, + { + "LoanRepayment": { + "Borrower": "0:95b13f0e84671d294ae84bf70a7e59f1c5963c485ed1eed7b2e1fe8d583bbcab", + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Loan": "-1:ee9ab316f8ea953833267a1efc9bf973bc9346d02781f2c6ae2601eb59923ea7", + "RoundSince": 1791971080, + "Amount": 1870000000000000, + "Accrued": 166933201032707, + "Recovered": 2039594284238138, + "PoolReward": 1352872211594, + "ProtocolFee": 0, + "BorrowerPayout": 1289864469718, + "BurnAmount": 18058665452, + "Shortfall": null, + "Implementation": "hipo" + }, + "Success": true, + "Type": "LoanRepayment", + "BaseTransactions": [ + "b5627daaaab38e1629e610008f27285d87a34dd84df4d9962897382502d6c924", + "f3d0977269d85a1e9939b7b6011375a7c5977fe9ad7dad5b21f3f77064ffc254", + "6bfb66cd77796599ccc953a319491c6f029fc5a72037387f5f59f2c11971a82e", + "c0bfd20a949523d6ba8b91e8b7efcbb9348fc12f9495ed2a8f000fc87dbc5f89" + ] + }, + { + "DepositStake": { + "Staker": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "Amount": { + "Currency": { + "Type": "native", + "Jetton": null, + "CurrencyID": null + }, + "Amount": 18529644780 + }, + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Implementation": "hipo" + }, + "Success": true, + "Type": "DepositStake", + "BaseTransactions": [ + "ae97030635ac673eec19c04631b203a7570c1fbf248a31ae135dbffca37f346b", + "80e85ade708dff76972dc4f7e4b97757d985f769c2aa45ab2915846c5e23d767" + ] + }, + { + "JettonMint": { + "Jetton": "0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87", + "Recipient": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "RecipientsWallet": "0:08a58b6cf90f80e4248fd7c21033c4e3e70b1e6ee0b5c12642343880f1e53404", + "Amount": "15647183422" + }, + "Success": true, + "Type": "JettonMint", + "BaseTransactions": [ + "5aaa0f09b134c3595fed419ef79ba7e37ba9fbdd3fef4f5873752a2e97632155", + "725af49be291269a270d0a33a9d1a94f16011154b73db50ec0b5577ddc3a0e12" + ] + }, + { + "JettonSwap": { + "Dex": "dedust", + "UserWallet": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "Router": "0:9726eef35014082ddcedd67233cff3610df559f85c8476771c1436033d1680a9", + "In": { + "Amount": 15647183422, + "IsTon": false, + "JettonMaster": "0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87", + "JettonWallet": "0:08a58b6cf90f80e4248fd7c21033c4e3e70b1e6ee0b5c12642343880f1e53404" + }, + "Out": { + "Amount": 6277889905227, + "IsTon": false, + "JettonMaster": "0:d0114af42cf8bc9ba0fa1745abbeea24eed65b01caf4ca7d98ee12b6f7530ccc", + "JettonWallet": "0:a16cdaf289f76a01ad6f168ac6f6220f48db6406a11938d218674768d1f10bb2" + } + }, + "Success": true, + "Type": "JettonSwap", + "BaseTransactions": [ + "fa079572ed1d54079203752f15b6e5f91820f273d554e4d0a3c4250c3c4033f9", + "03d31ffed6f64318e1b354301c3b47db8b09018a0c81be18c3dbd92336655f22", + "714688ae627baf1acd0f5878a5071cda72dca60a95927e9ca63d9d25f5b058c1", + "d648bb50b9f8b8295dd35878c228b6a4f5fea03e404665278bb6b64c9cb5985d", + "038212fa7f422c41ea790785ac5911a9a15688e9d9978a05de28534bc59c98af", + "8f2278987439b95bcb57b4b4eec2912e8f0c69de1d9b0682736ad6070f62a13c", + "889de8c500191f80b52cc36ebfe4296f77d8a8acd12a9144afbd29dba007f694", + "252b26d9e926358a6d6eae7b03b7789e5c1d01352cf7dd21d3a0ad4192bf32e5", + "728af9ff181a37c1031b62f38a984075e5600d752aa3159b55f696c7e4206afe", + "300dc78bf195cde26251847d48f2609f11e43ec2d7a0525544dedc375151d5aa" + ] + }, + { + "JettonBurn": { + "Jetton": "0:d0114af42cf8bc9ba0fa1745abbeea24eed65b01caf4ca7d98ee12b6f7530ccc", + "Sender": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "SendersWallet": "0:cc67b804fa5ee26421a1dbc8da5810a4447bf4cc697bb3da858b80684bcd323a", + "Amount": "6277889905227", + "Owner": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030" + }, + "Success": true, + "Type": "JettonBurn", + "BaseTransactions": [ + "191b2932e5542c597d761e6c100566865b158d6668554b0c6c6613afbeb27945", + "3bf83f3ff2788fce5af7142d0e98eeb2b5ab9b19fb18a22946df3d26d2eacf8d" + ] + }, + { + "SmartContractExec": { + "TonAttached": 98985330, + "Executor": "0:d0114af42cf8bc9ba0fa1745abbeea24eed65b01caf4ca7d98ee12b6f7530ccc", + "Contract": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "Operation": "Excess", + "Payload": "QueryId: 391\n" + }, + "Success": true, + "Type": "SmartContractExec", + "BaseTransactions": [ + "e262432a9ec642fc5ea24b2f58391f7b04e43dd356847d34ac6be4d19827cc9c" + ] + }, + { + "ElectionsRecoverStake": { + "Amount": 2039594349848138, + "Elector": "-1:3333333333333333333333333333333333333333333333333333333333333333", + "Staker": "-1:658046730ded813ea99f58128a179b7058f53c21c289ff3fc809cc5279f30900" + }, + "Success": true, + "Type": "ElectionsRecoverStake", + "BaseTransactions": [ + "e9cc9589a9832c3dd850e5d35d39eba47584e8b03ed4106e7fdbad81cca19203", + "e974f27f1b1583d6fa5ed882854412b4df4acb442f945abc70ca7a2b628f967a" + ] + }, + { + "LoanRepayment": { + "Borrower": "0:d368e77f6dae535e64478539803a2f71f07dd7b9155482057a2a7723ee168986", + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Loan": "-1:658046730ded813ea99f58128a179b7058f53c21c289ff3fc809cc5279f30900", + "RoundSince": 1791971080, + "Amount": 1870000000000000, + "Accrued": 166933201032707, + "Recovered": 2039594284238138, + "PoolReward": 1352872211594, + "ProtocolFee": 0, + "BorrowerPayout": 1289864469718, + "BurnAmount": 18058665452, + "Shortfall": null, + "Implementation": "hipo" + }, + "Success": true, + "Type": "LoanRepayment", + "BaseTransactions": [ + "b630711dfceb7619bb74cb46888f0d9a07f51af3c4d1b5af1e3474feb804d79c", + "f1c1a46cb29a21d0e35ffee14d990fc166fa94733f8442e7bbfb3d8b260926af", + "d1250a4d5cf6edd645177bcf7af9e2b86ef5a9714953a498ccc354b346322a03", + "f53ca868519e2af8cf42f7789a1c9bbfd0193c4fc59814dd9ccaa665bfbb3246" + ] + }, + { + "PoolRoundTransition": { + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "RoundSince": 1791971080, + "Transition": "settled", + "TotalStaked": 4073866402065414, + "TotalRecovered": 4076572146488602, + "Implementation": "hipo" + }, + "Success": true, + "Type": "PoolRoundTransition", + "BaseTransactions": [ + "28e5c11b8b448614fb4f83e84ebf1c61205ccb35b599e18ff0b1caa45534aa50", + "1f47d8ccd8d4281bdd30bd387645b6c75de69e9160ebb4f9a74d20d005239522", + "7560b2a733326198cc2265d59a80470f4b8f5ea65d56046654625eb8d75a99db", + "7af0c6183171ead098cc83cc80f466cbed54a5bb336e926b4c323d7761a06e43" + ] + }, + { + "WithdrawStake": { + "Staker": "0:a732d044d31f709b6174024d7ae1898110f672c38443f2d5ae853a0ac474c893", + "Amount": 3755037590918, + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Implementation": "hipo" + }, + "Success": true, + "Type": "WithdrawStake", + "BaseTransactions": [ + "437f00c428d903329ed633c0596efee698ec7c1577ede82095a8f2b8c64d4ff4", + "215cf2cff3b0723e0ca246d58b1aaa2601fd85ecf72053657e2424a90f8d9349", + "64ac2c7ea0c82c6bf68c193df45ade72375bfe3596585fb1f37ba0c7ef6a1111", + "d2bddd58b3ff96dd49188082dd1ab1191260070e3ba993759aef2b1a65261b5d" + ] + }, + { + "DepositStake": { + "Staker": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "Amount": { + "Currency": { + "Type": "native", + "Jetton": null, + "CurrencyID": null + }, + "Amount": 17557118581 + }, + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Implementation": "hipo" + }, + "Success": true, + "Type": "DepositStake", + "BaseTransactions": [ + "59b63ad2c86d5d3f553df2f4b3e72a359f504ca95f261d233f2600e0a641fc58", + "e492243f5ace43707557fd1b8023d8ca97c26f8d097d1d37bb5f25b270744e01" + ] + }, + { + "JettonMint": { + "Jetton": "0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87", + "Recipient": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "RecipientsWallet": "0:08a58b6cf90f80e4248fd7c21033c4e3e70b1e6ee0b5c12642343880f1e53404", + "Amount": "14825942864" + }, + "Success": true, + "Type": "JettonMint", + "BaseTransactions": [ + "c1e37508ccf4bb90522a42d8fd5a3e09f779f4fc3a81cadfa27606e486908fc2", + "b74e582a710b94e879691b8d871bd4afd2fa29d690d5535fd00946185d8e9de4" + ] + }, + { + "JettonSwap": { + "Dex": "dedust", + "UserWallet": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "Router": "0:9726eef35014082ddcedd67233cff3610df559f85c8476771c1436033d1680a9", + "In": { + "Amount": 14825942864, + "IsTon": false, + "JettonMaster": "0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87", + "JettonWallet": "0:08a58b6cf90f80e4248fd7c21033c4e3e70b1e6ee0b5c12642343880f1e53404" + }, + "Out": { + "Amount": 5833803953619, + "IsTon": false, + "JettonMaster": "0:d0114af42cf8bc9ba0fa1745abbeea24eed65b01caf4ca7d98ee12b6f7530ccc", + "JettonWallet": "0:a16cdaf289f76a01ad6f168ac6f6220f48db6406a11938d218674768d1f10bb2" + } + }, + "Success": true, + "Type": "JettonSwap", + "BaseTransactions": [ + "b7658829c1534f1a89510aaa3a0d11bb7acb85c47301417bbd35b01d922d93f8", + "56c295d3d31e342a69d169bc93d7d43a2910a5f90fcb8df5d611efadfdcb8446", + "8bb71ebc21bf2c52bf6fa73163213f9fe1d4994f08e535e16d654b906be5e91a", + "ac4b43ac477e5e57cde659d46701b3b94057f8c69b73524f3928ac4aa417c75d", + "b7e0bd6877eab27276af832e0776a4784f551da50f1150b7cbda00fa4c89e753", + "00dc232836ab9585fe0fa458101764f32bd46f09046ba315a776e8ace3c7230f", + "87b32017dd569690b16304de9626859dfc218200f190bbdc08c200e647b317bd", + "d2c793dc4213369278ccf4b7b02d38d8efdd7dfcdf33f3924f3b51afdf284078", + "0457d08918e0d49dfb893c35b45a49e1c6b05c739401fdde48919f969379e533", + "59ad7d89b7be0eb999be78143628aa8628d77d44b995bc0d1532b4dd489e8485" + ] + }, + { + "JettonBurn": { + "Jetton": "0:d0114af42cf8bc9ba0fa1745abbeea24eed65b01caf4ca7d98ee12b6f7530ccc", + "Sender": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "SendersWallet": "0:cc67b804fa5ee26421a1dbc8da5810a4447bf4cc697bb3da858b80684bcd323a", + "Amount": "5833803953619", + "Owner": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030" + }, + "Success": true, + "Type": "JettonBurn", + "BaseTransactions": [ + "df06ba14dfcf8232f6abadf3920a1df9d6ac1ce6c0393c2ad46a587249c867b1", + "c513b710a64de3accb7ac8e9d5792a80cebb880bb6abc8b28e5efc4f2a0d543c" + ] + }, + { + "SmartContractExec": { + "TonAttached": 98985330, + "Executor": "0:d0114af42cf8bc9ba0fa1745abbeea24eed65b01caf4ca7d98ee12b6f7530ccc", + "Contract": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "Operation": "Excess", + "Payload": "QueryId: 392\n" + }, + "Success": true, + "Type": "SmartContractExec", + "BaseTransactions": [ + "d25e65cc247a99792e7e375852e197bb91b613b252c9750fecbef57ed4bbf7ac" + ] + } + ], + "Accounts": [ + { + "Account": "-1:3333333333333333333333333333333333333333333333333333333333333333", + "Ton": -4079186293286326, + "Fee": 0, + "Jettons": null + }, + { + "Account": "-1:658046730ded813ea99f58128a179b7058f53c21c289ff3fc809cc5279f30900", + "Ton": -983080000, + "Fee": 123343692, + "Jettons": null + }, + { + "Account": "-1:ee9ab316f8ea953833267a1efc9bf973bc9346d02781f2c6ae2601eb59923ea7", + "Ton": -983080000, + "Fee": 123343692, + "Jettons": null + }, + { + "Account": "0:08a58b6cf90f80e4248fd7c21033c4e3e70b1e6ee0b5c12642343880f1e53404", + "Ton": 0, + "Fee": 2970278, + "Jettons": null + }, + { + "Account": "0:50e74913216cb06936b4d69fc99c78646a212e93076dd563ef74722f3f557948", + "Ton": 9734, + "Fee": 1471471, + "Jettons": [ + { + "Address": "0:d0114af42cf8bc9ba0fa1745abbeea24eed65b01caf4ca7d98ee12b6f7530ccc", + "Quantity": -12111693858846 + } + ] + }, + { + "Account": "0:7e40f97d20d6eea148846608cd6e0163e1325f5812392cdee307b5c96508c68c", + "Ton": 0, + "Fee": 1419298, + "Jettons": null + }, + { + "Account": "0:8b5fe4adec8f730419485effa12b22010f31fb02d7c14f6ec32c8a7c0cd935e7", + "Ton": -96528855, + "Fee": 553073, + "Jettons": null + }, + { + "Account": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Ton": 4072852711234669, + "Fee": 32039647, + "Jettons": null + }, + { + "Account": "0:918c82e6243d190fbcba3ce17289c0b1db43e8ca9904c0727ae00309eefc245c", + "Ton": 0, + "Fee": 1442271, + "Jettons": [ + { + "Address": "0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87", + "Quantity": 30473126286 + } + ] + }, + { + "Account": "0:95b13f0e84671d294ae84bf70a7e59f1c5963c485ed1eed7b2e1fe8d583bbcab", + "Ton": 1289864334012, + "Fee": 69039, + "Jettons": null + }, + { + "Account": "0:9726eef35014082ddcedd67233cff3610df559f85c8476771c1436033d1680a9", + "Ton": 0, + "Fee": 3009609, + "Jettons": null + }, + { + "Account": "0:a16cdaf289f76a01ad6f168ac6f6220f48db6406a11938d218674768d1f10bb2", + "Ton": -1, + "Fee": 1562005, + "Jettons": null + }, + { + "Account": "0:a62c2600bb53e19646f771420a0c5c03a417cfdaf5c30a31425268298a6091ef", + "Ton": 567157681, + "Fee": 390178, + "Jettons": null + }, + { + "Account": "0:a732d044d31f709b6174024d7ae1898110f672c38443f2d5ae853a0ac474c893", + "Ton": 3755037524336, + "Fee": 66582, + "Jettons": null + }, + { + "Account": "0:cc67b804fa5ee26421a1dbc8da5810a4447bf4cc697bb3da858b80684bcd323a", + "Ton": 0, + "Fee": 2679348, + "Jettons": null + }, + { + "Account": "0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87", + "Ton": -171, + "Fee": 1825444, + "Jettons": null + }, + { + "Account": "0:d0114af42cf8bc9ba0fa1745abbeea24eed65b01caf4ca7d98ee12b6f7530ccc", + "Ton": -3646, + "Fee": 1037916, + "Jettons": null + }, + { + "Account": "0:d368e77f6dae535e64478539803a2f71f07dd7b9155482057a2a7723ee168986", + "Ton": 1289864334012, + "Fee": 69039, + "Jettons": null + }, + { + "Account": "0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030", + "Ton": 3379, + "Fee": 11608364, + "Jettons": null + }, + { + "Account": "0:e5524d594d0bf39cd7e812aebe73b8e0f36701b76aed35bfa00a26b3b30f7309", + "Ton": 17085, + "Fee": 537672, + "Jettons": null + }, + { + "Account": "0:faaa4eab4b5d12f26010a042f88337d79b8d70aec42c24ae1fdce934cefc0216", + "Ton": 0, + "Fee": 1925473, + "Jettons": null + } + ] + } \ No newline at end of file diff --git a/pkg/bath/testdata/hipo-round-validated.json b/pkg/bath/testdata/hipo-round-validated.json new file mode 100644 index 000000000..c2b2c0e21 --- /dev/null +++ b/pkg/bath/testdata/hipo-round-validated.json @@ -0,0 +1,27 @@ +{ + "Actions": [ + { + "PoolRoundTransition": { + "Pool": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "RoundSince": 1792036616, + "Transition": "validated", + "TotalStaked": null, + "TotalRecovered": null, + "Implementation": "hipo" + }, + "Success": true, + "Type": "PoolRoundTransition", + "BaseTransactions": [ + "57074455cf497e7cded8d790b8de2d6c7e5d6148252565de1d5eeee25020fc5d" + ] + } + ], + "Accounts": [ + { + "Account": "0:8bc991cfe177bc7e9721433efa3befd199485a55cffd040a06c89af026b71bcf", + "Ton": -621201, + "Fee": 621201, + "Jettons": null + } + ] + } \ No newline at end of file diff --git a/pkg/core/converters.go b/pkg/core/converters.go index 207450c48..bac654f6a 100644 --- a/pkg/core/converters.go +++ b/pkg/core/converters.go @@ -12,6 +12,7 @@ import ( "github.com/tonkeeper/tongo/ton" "github.com/tonkeeper/opentonapi/internal/g" + "github.com/tonkeeper/opentonapi/pkg/references" ) func ConvertToBlockHeader(id tongo.BlockIDExt, block *tlb.Block) (*BlockHeader, error) { @@ -367,6 +368,10 @@ func ConvertMessage(message tlb.Message, txLT uint64, cd *abi.ContractDescriptio case "ExtOutMsgInfo": var decodedBody *DecodedMessageBody info := message.Info.ExtOutMsgInfo + source, err := ton.AccountIDFromTlb(info.Src) + if err != nil { + return Message{}, err + } tag, op, value, err := abi.ExtOutMessageDecoder(&cell, ci, info.Dest) if err == nil && op != nil { decodedBody = &DecodedMessageBody{ @@ -374,9 +379,20 @@ func ConvertMessage(message tlb.Message, txLT uint64, cd *abi.ContractDescriptio Value: value, } } - source, err := ton.AccountIDFromTlb(info.Src) - if err != nil { - return Message{}, err + // Hipo's treasury and burner log by topic, which abi cannot read, and the first 32 + // bits of the body are not an op-code. + // body cannot be reused: convertBodyCell keeps some bodies as text, so the log is + // serialized from the message cell. + if source != nil && (*source == references.HipoTreasury || *source == references.HipoBurner) { + logCell := boc.Cell(message.Body.Value) + if logBody, err := logCell.ToBoc(); err == nil { + dest := externalAddressFromTlb(info.Dest) + if log, ok := decodeHipoLog(source, dest, logBody, info.CreatedAt); ok { + tag, decodedBody = nil, log + } else if log, ok := decodeHipoBurnerLog(source, dest, logBody); ok { + tag, decodedBody = nil, log + } + } } return Message{ MessageID: MessageID{ diff --git a/pkg/core/hipo_logs.go b/pkg/core/hipo_logs.go new file mode 100644 index 000000000..29e41d6d7 --- /dev/null +++ b/pkg/core/hipo_logs.go @@ -0,0 +1,371 @@ +package core + +import ( + "fmt" + "reflect" + + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +// Hipo's treasury reports what a round did in external-out messages rather than in the +// messages it sends, and the two carry different things. reserve_tokens, loan_result and +// recover_stake_result carry a query id and an address; how much a loan repaid, and how +// that repayment was split between stakers, the governor, the borrower and the burner, +// exists only here. +// +// The shape is unusual enough to be worth stating. emit_log puts the topic in the message's +// destination - a 256-bit external address holding a small integer - and starts the body at +// round_since with no op-code at all. Decoders that key external-out messages on a body +// op-code therefore read round_since as one, and report a different op-code for the same +// log every round. These are parsed by topic instead. +// +// Layouts follow log_loan, log_repayment and log_finish in contracts/imports/utils.fc, and +// log_deficit in treasury.fc. Amounts are Coins; borrower_reward_share and borrower_fee are +// rates out of 65535, not amounts. + +const ( + HipoLogLoan HipoLogTopic = 1 + HipoLogRepayment HipoLogTopic = 2 + HipoLogFinish HipoLogTopic = 3 + HipoLogFailedBurningTokens HipoLogTopic = 4 + HipoLogDeficit HipoLogTopic = 5 +) + +// Two of these logs changed shape with Hipo's 2026-09-05 release, which introduced the +// borrower fee. Both layouts are parsed, because actions are recomputed from history and a +// round settled before that release would otherwise be unreadable. The layout is chosen by +// when the log was written, not by which one fits: an old body can fit the new layout +// exactly and read as nonsense, and 8 of the 542 real logs around the release fit both. +// hipoBorrowerFeeRelease is the treasury's upgrade transaction (9722cf28d0be9a08...) on +// 2026-09-05 at 17:29:33 UTC; every log written after it is in the new layout. +const hipoBorrowerFeeRelease = 1788629373 + +// each log is named the way a decoded message is named +var hipoLogOperations = map[HipoLogTopic]string{ + HipoLogLoan: "HipoFinanceLogLoan", + HipoLogRepayment: "HipoFinanceLogRepayment", + HipoLogFinish: "HipoFinanceLogFinish", + HipoLogFailedBurningTokens: "HipoFinanceLogFailedBurningTokens", + HipoLogDeficit: "HipoFinanceLogDeficit", +} + +var hipoBurnerLogs = map[uint64]struct { + operation string + layout func() any +}{ + 1: {"HipoBurnerLogReceived", func() any { return &HipoBurnerReceivedLog{} }}, + 2: {"HipoBurnerLogDeposit", func() any { return &HipoBurnerDepositLog{} }}, + 3: {"HipoBurnerLogSwap", func() any { return &HipoBurnerSwapLog{} }}, + 4: {"HipoBurnerLogBurn", func() any { return &HipoBurnerBurnLog{} }}, + 5: {"HipoBurnerLogDiscovery", func() any { return &HipoBurnerDiscoveryLog{} }}, + 6: {"HipoBurnerLogUpgrade", func() any { return &HipoBurnerUpgradeLog{} }}, +} + +type HipoLogTopic uint64 + +// HipoLoanLog is emitted once per loan the treasury hands out, as the round is distributed. +type HipoLoanLog struct { + RoundSince uint32 + MinPayment tlb.Grams + // BorrowerRewardShare is always scaled to 65535 here. Before 2026-09-05 the field was 8 + // bits out of 255, and is converted on the way out by the factor the contract's own + // migration note gives: 255 * 257 = 65535. + BorrowerRewardShare uint16 + LoanAmount tlb.Grams + AccrueAmount tlb.Grams + StakeAmount tlb.Grams + // BorrowerFee is a rate out of 65535, snapshotted from the extension when the request + // was made so that set_borrower_fee cannot reprice a committed loan. Zero for a loan + // made before the fee existed, which is accurate rather than missing. + BorrowerFee uint16 + Borrower tlb.MsgAddress +} + +// HipoRepaymentLog is emitted once per loan recovered, and is the only place the split of a +// repayment appears. StakersShare is what reaches the exchange rate. +type HipoRepaymentLog struct { + RoundSince uint32 + RepaymentAmount tlb.Grams + LoanAmount tlb.Grams + AccrueAmount tlb.Grams + StakersShare tlb.Grams + GovernorShare tlb.Grams + BorrowerShare tlb.Grams + // BurnShare is zero for a loan recovered before 2026-09-05, when no share was burned. + BurnShare tlb.Grams + Borrower tlb.MsgAddress +} + +// HipoFinishLog is emitted once per round, when its last loan has been recovered. +type HipoFinishLog struct { + RoundSince uint32 + TotalStaked tlb.Grams + TotalRecovered tlb.Grams + TotalCoins tlb.Grams + TotalTokens tlb.Grams +} + +// HipoFailedBurningLog is emitted when a bill burns that the treasury cannot settle. +type HipoFailedBurningLog struct { + RoundSince uint32 + TotalCoins tlb.Grams + TotalTokens tlb.Grams + Coins tlb.Grams + Tokens tlb.Grams + Owner tlb.MsgAddress +} + +// HipoDeficitLog is emitted alongside a repayment that did not cover what it owed. +type HipoDeficitLog struct { + RoundSince uint32 + LoanAmount tlb.Grams + AccrueAmount tlb.Grams + IncomingTon tlb.Grams + Shortfall tlb.Grams + Deficit tlb.Grams + Borrower tlb.MsgAddress +} + +// Hipo's burner logs the same way, with topics of its own (contracts/burner.fc in +// HipoFinance/burner). Each log is read only from the burner. +type HipoBurnerReceivedLog struct { + Op uint32 + Value tlb.Grams + TotalReceived tlb.Grams + Source tlb.MsgAddress +} + +type HipoBurnerDepositLog struct { + QueryId uint64 + Stake tlb.Grams +} + +type HipoBurnerSwapLog struct { + QueryId uint64 + Amount tlb.Grams + TotalSwapped tlb.Grams +} + +type HipoBurnerBurnLog struct { + QueryId uint64 + Amount tlb.Grams + TotalBurned tlb.Grams +} + +type HipoBurnerDiscoveryLog struct { + QueryId uint64 +} + +type HipoBurnerUpgradeLog struct { + QueryId uint64 + Source tlb.MsgAddress +} + +// the pre-2026-09-05 shapes, kept only so that history still parses +type hipoLoanLogV1 struct { + RoundSince uint32 + MinPayment tlb.Grams + BorrowerRewardShare uint8 + LoanAmount tlb.Grams + AccrueAmount tlb.Grams + StakeAmount tlb.Grams + Borrower tlb.MsgAddress +} + +type hipoRepaymentLogV1 struct { + RoundSince uint32 + RepaymentAmount tlb.Grams + LoanAmount tlb.Grams + AccrueAmount tlb.Grams + StakersShare tlb.Grams + GovernorShare tlb.Grams + BorrowerShare tlb.Grams + Borrower tlb.MsgAddress +} + +func DecodeHipoLog(msg Message) (HipoLogTopic, any, bool) { + topic, ok := hipoLogTopicOf(msg) + if !ok { + return 0, nil, false + } + // ConvertMessage has already decoded the logs of the treasury it saw; a store may not have. + if msg.DecodedBody != nil && msg.DecodedBody.Operation == hipoLogOperations[topic] { + switch msg.DecodedBody.Value.(type) { + case HipoLoanLog, HipoRepaymentLog, HipoFinishLog, HipoFailedBurningLog, HipoDeficitLog: + return topic, msg.DecodedBody.Value, true + } + } + value, err := parseHipoLog(topic, msg.Body, msg.CreatedAt) + if err != nil { + return 0, nil, false + } + return topic, value, true +} + +func hipoLogTopicOf(msg Message) (HipoLogTopic, bool) { + // The topic is just an address any contract can write to: callers must check that the + // treasury wrote the message. + if msg.MsgType != ExtOutMsg { + return 0, false + } + return hipoTopic(msg.DestinationExtern) +} + +func decodeHipoLog(source *ton.AccountID, dest *ExternalAddress, body []byte, createdAt uint32) (*DecodedMessageBody, bool) { + // abi cannot decode these: it dispatches on a body op-code, and these bodies start at + // round_since. Only the treasury's own messages are read. + if source == nil || *source != references.HipoTreasury { + return nil, false + } + topic, ok := hipoTopic(dest) + if !ok { + return nil, false + } + value, err := parseHipoLog(topic, body, createdAt) + if err != nil { + return nil, false + } + return &DecodedMessageBody{Operation: hipoLogOperations[topic], Value: value}, true +} + +func decodeHipoBurnerLog(source *ton.AccountID, dest *ExternalAddress, body []byte) (*DecodedMessageBody, bool) { + if source == nil || *source != references.HipoBurner { + return nil, false + } + topic, ok := hipoTopicValue(dest) + if !ok { + return nil, false + } + log, ok := hipoBurnerLogs[topic] + if !ok { + return nil, false + } + value := log.layout() + if unmarshalHipoLog(body, value) != nil { + return nil, false + } + return &DecodedMessageBody{Operation: log.operation, Value: reflect.ValueOf(value).Elem().Interface()}, true +} + +func parseHipoLog(topic HipoLogTopic, body []byte, createdAt uint32) (any, error) { + switch topic { + case HipoLogLoan: + return parseHipoLoanLog(body, createdAt) + case HipoLogRepayment: + return parseHipoRepaymentLog(body, createdAt) + case HipoLogFinish: + var log HipoFinishLog + err := unmarshalHipoLog(body, &log) + return log, err + case HipoLogFailedBurningTokens: + var log HipoFailedBurningLog + err := unmarshalHipoLog(body, &log) + return log, err + case HipoLogDeficit: + var log HipoDeficitLog + err := unmarshalHipoLog(body, &log) + return log, err + } + return nil, fmt.Errorf("unknown hipo log topic %d", topic) +} + +func parseHipoLoanLog(body []byte, createdAt uint32) (HipoLoanLog, error) { + if hipoCurrentLayout(createdAt) { + var log HipoLoanLog + err := unmarshalHipoLog(body, &log) + return log, err + } + var old hipoLoanLogV1 + if err := unmarshalHipoLog(body, &old); err != nil { + return HipoLoanLog{}, err + } + return HipoLoanLog{ + RoundSince: old.RoundSince, + MinPayment: old.MinPayment, + // 255 -> 65535, the conversion the contract's own note prescribes. + BorrowerRewardShare: uint16(old.BorrowerRewardShare) * 257, + LoanAmount: old.LoanAmount, + AccrueAmount: old.AccrueAmount, + StakeAmount: old.StakeAmount, + Borrower: old.Borrower, + }, nil +} + +func parseHipoRepaymentLog(body []byte, createdAt uint32) (HipoRepaymentLog, error) { + if hipoCurrentLayout(createdAt) { + var log HipoRepaymentLog + err := unmarshalHipoLog(body, &log) + return log, err + } + var old hipoRepaymentLogV1 + if err := unmarshalHipoLog(body, &old); err != nil { + return HipoRepaymentLog{}, err + } + return HipoRepaymentLog{ + RoundSince: old.RoundSince, + RepaymentAmount: old.RepaymentAmount, + LoanAmount: old.LoanAmount, + AccrueAmount: old.AccrueAmount, + StakersShare: old.StakersShare, + GovernorShare: old.GovernorShare, + BorrowerShare: old.BorrowerShare, + Borrower: old.Borrower, + }, nil +} + +func hipoCurrentLayout(createdAt uint32) bool { + // A log of unknown time, 0, is taken as current: a store that does not keep the time of + // external-out messages would otherwise misread every new log. + return createdAt == 0 || createdAt >= hipoBorrowerFeeRelease +} + +func hipoTopic(dest *ExternalAddress) (HipoLogTopic, bool) { + value, ok := hipoTopicValue(dest) + if !ok { + return 0, false + } + switch topic := HipoLogTopic(value); topic { + case HipoLogLoan, HipoLogRepayment, HipoLogFinish, HipoLogFailedBurningTokens, HipoLogDeficit: + return topic, true + } + return 0, false +} + +func hipoTopicValue(dest *ExternalAddress) (uint64, bool) { + if dest == nil || dest.BitsAvailableForRead() != 256 { + return 0, false + } + copied := dest.Copy() + // ReadUint stops at 64 bits. + value, err := copied.ReadBigUint(256) + if err != nil || !value.IsUint64() { + return 0, false + } + return value.Uint64(), true +} + +func unmarshalHipoLog(body []byte, dst any) error { + if len(body) == 0 { + return fmt.Errorf("empty body") + } + cells, err := boc.DeserializeBoc(body) + if err != nil { + return err + } + if len(cells) != 1 { + return fmt.Errorf("expected one root cell, got %d", len(cells)) + } + if err := tlb.Unmarshal(cells[0], dst); err != nil { + return err + } + // The body must be consumed exactly. That is the only defence against a layout that has + // drifted from the contract: a field read at the wrong width still decodes and still + // yields a plausible number, and nothing but the leftover bits says otherwise. + if left := cells[0].BitsAvailableForRead(); left != 0 { + return fmt.Errorf("%d bits left unread: the layout does not match the contract", left) + } + return nil +} diff --git a/pkg/core/hipo_logs_test.go b/pkg/core/hipo_logs_test.go new file mode 100644 index 000000000..d62a71826 --- /dev/null +++ b/pkg/core/hipo_logs_test.go @@ -0,0 +1,243 @@ +package core + +import ( + "encoding/hex" + "encoding/json" + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +// Real logs, taken from the treasury's own transactions on mainnet and identified by the +// topic in their destination address. Each is checked field by field against the log_* +// functions in the Hipo contracts, and unmarshalHipoLog additionally insists the body was +// consumed exactly - which is what would catch a layout drifting from the contract. +const ( + hipoLoanLogBody = "b5ee9c720101010100460000876a9e4f0850ba43b740007077038d7ea4c68000708f0db1157feef523aff0d8e07fff801a6d1cefedb5ca6bcc88f0a7300745ee3e0fbaf722aa9040af454ee47dc2d130d0" + hipoRepaymentLogBody = "b5ee9c7201010101004b0000916a944f08707268034320633703328b944c4000703f288f2fa449a601375194ce1c05344a2f57fd8007b591d71001a927c6fc6b00e107ffea04e4a60f955f4f6c6475b5d6c02703bcb0" + hipoFinishLogBody = "b5ee9c720101010100240000446a944f0870e4a290e8d093470e4c97b1b6a56c71c940a9e703cb07189eaf16dfb68a" +) + +func TestHipoParseLoanLog(t *testing.T) { + log, err := parseHipoLoanLog(mustHex(t, hipoLoanLogBody), hipoBorrowerFeeRelease+1) + require.NoError(t, err) + require.Equal(t, uint32(1788759816), log.RoundSince) + // A rate out of 65535, not an amount: 1799 is the share this borrower bid. + require.Equal(t, uint16(1799), log.BorrowerRewardShare) + require.EqualValues(t, "AddrStd", log.Borrower.SumType) + // The loan is the pool's money; the stake is the borrower's own, posted alongside it. + require.Greater(t, uint64(log.LoanAmount), uint64(0)) + require.Greater(t, uint64(log.StakeAmount), uint64(0)) + t.Logf("loan log: round=%d min_payment=%d share=%d loan=%d accrue=%d stake=%d fee=%d", + log.RoundSince, log.MinPayment, log.BorrowerRewardShare, log.LoanAmount, + log.AccrueAmount, log.StakeAmount, log.BorrowerFee) +} + +func TestHipoParseRepaymentLog(t *testing.T) { + // Recovered before 2026-09-05, so it is in the older six-amount layout and BurnShare + // is legitimately zero. + log, err := parseHipoRepaymentLog(mustHex(t, hipoRepaymentLogBody), hipoBorrowerFeeRelease-1) + require.NoError(t, err) + require.Zero(t, uint64(log.BurnShare)) + require.Equal(t, uint32(1788104456), log.RoundSince) + require.EqualValues(t, "AddrStd", log.Borrower.SumType) + // This is the whole reason the logs are read: the split exists in no message. + require.Greater(t, uint64(log.RepaymentAmount), uint64(0)) + require.Greater(t, uint64(log.StakersShare), uint64(0)) + // A repayment covers the loan and then some, and what it pays out cannot exceed it. + require.GreaterOrEqual(t, uint64(log.RepaymentAmount), uint64(log.LoanAmount)) + split := uint64(log.StakersShare) + uint64(log.GovernorShare) + + uint64(log.BorrowerShare) + uint64(log.BurnShare) + require.LessOrEqual(t, split, uint64(log.RepaymentAmount)) + t.Logf("repayment log: round=%d repaid=%d loan=%d accrue=%d stakers=%d governor=%d borrower=%d burn=%d", + log.RoundSince, log.RepaymentAmount, log.LoanAmount, log.AccrueAmount, + log.StakersShare, log.GovernorShare, log.BorrowerShare, log.BurnShare) +} + +func TestHipoParseFinishLog(t *testing.T) { + var log HipoFinishLog + require.NoError(t, unmarshalHipoLog(mustHex(t, hipoFinishLogBody), &log)) + require.Equal(t, uint32(1788104456), log.RoundSince) + // total_coins and total_tokens are the exchange rate at the moment the round finished, + // so both have to be there for the rate to be derivable. + require.Greater(t, uint64(log.TotalCoins), uint64(0)) + require.Greater(t, uint64(log.TotalTokens), uint64(0)) + // A round that was lent out and recovered more than it staked is the normal case. + require.GreaterOrEqual(t, uint64(log.TotalRecovered), uint64(log.TotalStaked)) + t.Logf("finish log: round=%d staked=%d recovered=%d total_coins=%d total_tokens=%d rate=%.6f", + log.RoundSince, log.TotalStaked, log.TotalRecovered, log.TotalCoins, log.TotalTokens, + float64(log.TotalCoins)/float64(log.TotalTokens)) +} + +func TestHipoParseLogRejectsShortBody(t *testing.T) { + // a body that is one field short of its layout must be refused rather than half-read + var log HipoFinishLog + require.Error(t, unmarshalHipoLog(mustHex(t, hipoRepaymentLogBody), &log), + "a repayment body read as a finish log leaves bits over and must not be accepted") +} + +func TestHipoParseAllLogsInCorpus(t *testing.T) { + // No body anywhere may be left half-read, since a half-read body is exactly what a + // layout that has drifted from the contract looks like. Both logs changed shape with the + // 2026-09-05 borrower-fee release, and the corpus spans it, so this also covers the + // fallback to the older layouts. + dir := os.Getenv("REPLAY_DIR") + if dir == "" { + t.Skip("set REPLAY_DIR to the directory holding log_samples.json") + } + raw, err := os.ReadFile(filepath.Join(dir, "log_samples.json")) + if err != nil { + t.Skipf("no corpus: %v", err) + } + byTopic := map[string][]string{} + require.NoError(t, json.Unmarshal(raw, &byTopic)) + counts := map[string]int{} + for topic, bodies := range byTopic { + for _, b := range bodies { + body := mustHex(t, b) + switch topic[len(topic)-1] { + case '1': + // the samples carry no time, so either era may be the right one + log, err := parseHipoLoanLog(body, hipoBorrowerFeeRelease) + if err != nil { + log, err = parseHipoLoanLog(body, 1) + } + require.NoErrorf(t, err, "loan log %s", b) + require.NotZero(t, log.RoundSince) + require.LessOrEqual(t, uint64(log.BorrowerRewardShare), uint64(65535)) + case '2': + log, err := parseHipoRepaymentLog(body, hipoBorrowerFeeRelease) + if err != nil { + log, err = parseHipoRepaymentLog(body, 1) + } + require.NoErrorf(t, err, "repayment log %s", b) + require.NotZero(t, log.RoundSince) + split := uint64(log.StakersShare) + uint64(log.GovernorShare) + + uint64(log.BorrowerShare) + uint64(log.BurnShare) + require.LessOrEqualf(t, split, uint64(log.RepaymentAmount), + "a repayment cannot pay out more than it brought in: %s", b) + case '3': + var log HipoFinishLog + require.NoErrorf(t, unmarshalHipoLog(body, &log), "finish log %s", b) + require.NotZero(t, log.TotalTokens) + default: + t.Fatalf("unexpected topic %s", topic) + } + counts[topic]++ + } + } + total := 0 + for topic, n := range counts { + t.Logf("topic ...%s: %d logs parsed, none left half-read", topic[len(topic)-4:], n) + total += n + } + require.Greater(t, total, 100, "corpus should not have shrunk") +} + +func TestHipoLogsDecodeOnlyFromTheTreasury(t *testing.T) { + // a topic is an address any contract can write to, so only the treasury's logs are decoded + topic := boc.NewBitString(256) + require.NoError(t, topic.WriteBigUint(big.NewInt(int64(HipoLogRepayment)), 256)) + body := mustHex(t, hipoRepaymentLogBody) + treasury := references.HipoTreasury + decoded, ok := decodeHipoLog(&treasury, &topic, body, hipoBorrowerFeeRelease-1) + require.True(t, ok) + require.Equal(t, "HipoFinanceLogRepayment", decoded.Operation) + require.IsType(t, HipoRepaymentLog{}, decoded.Value) + other := ton.MustParseAccountID("0:1111111111111111111111111111111111111111111111111111111111111111") + _, ok = decodeHipoLog(&other, &topic, body, hipoBorrowerFeeRelease-1) + require.False(t, ok) + // A body of another topic's layout is left undecoded rather than half-read. + finish := boc.NewBitString(256) + require.NoError(t, finish.WriteBigUint(big.NewInt(int64(HipoLogFinish)), 256)) + _, ok = decodeHipoLog(&treasury, &finish, body, hipoBorrowerFeeRelease-1) + require.False(t, ok) +} + +func TestHipoLoanLogEraIsByTime(t *testing.T) { + // An old loan log can fit the new layout exactly and read as nonsense, so the layout is + // chosen by when the log was written. The figures are such + // a log, one whose old fields also parse cleanly as the new layout. + borrower := ton.MustParseAccountID("0:7a228a801e58bdf435a39c93c8efb5a3736b58455381c04c7322cda3512b5bd4") + old := hipoLoanLogV1{ + RoundSince: 1788000000, + MinPayment: 544807109759, + BorrowerRewardShare: 17, + LoanAmount: 1662881220370992, + AccrueAmount: 124844006369212, + StakeAmount: 1486722724167, + Borrower: borrower.ToMsgAddress(), + } + cell := boc.NewCell() + require.NoError(t, tlb.Marshal(cell, old)) + body, err := cell.ToBoc() + require.NoError(t, err) + var misread HipoLoanLog + require.NoError(t, unmarshalHipoLog(body, &misread), "the old body fits the new layout too") + require.NotEqual(t, old.LoanAmount, misread.LoanAmount) + log, err := parseHipoLoanLog(body, hipoBorrowerFeeRelease-1) + require.NoError(t, err) + require.Equal(t, old.LoanAmount, log.LoanAmount) + require.Equal(t, old.AccrueAmount, log.AccrueAmount) + require.Equal(t, uint16(17*257), log.BorrowerRewardShare) + // a log of unknown time is taken as current + unknown, err := parseHipoLoanLog(body, 0) + require.NoError(t, err) + require.Equal(t, misread, unknown) +} + +func TestHipoBurnerLogsDecodeOnlyFromTheBurner(t *testing.T) { + topic := boc.NewBitString(256) + require.NoError(t, topic.WriteBigUint(big.NewInt(3), 256)) + // a real swap log, written when the burner sold 15.647 hGRAM + body := mustHex(t, "b5ee9c720101010100160000280000000000000185503a4a5123e60154ee22f17f") + burner := references.HipoBurner + decoded, ok := decodeHipoBurnerLog(&burner, &topic, body) + require.True(t, ok) + require.Equal(t, "HipoBurnerLogSwap", decoded.Operation) + require.Equal(t, HipoBurnerSwapLog{QueryId: 389, Amount: 15647183422, TotalSwapped: 1464284148095}, decoded.Value) + treasury := references.HipoTreasury + _, ok = decodeHipoBurnerLog(&treasury, &topic, body) + require.False(t, ok) +} + +func TestHipoDecodeLog(t *testing.T) { + topic := boc.NewBitString(256) + require.NoError(t, topic.WriteBigUint(big.NewInt(int64(HipoLogRepayment)), 256)) + msg := Message{MsgType: ExtOutMsg, DestinationExtern: &topic, Body: mustHex(t, hipoRepaymentLogBody), CreatedAt: hipoBorrowerFeeRelease - 1} + t.Run("parses the body when nothing decoded it", func(t *testing.T) { + got, value, ok := DecodeHipoLog(msg) + require.True(t, ok) + require.Equal(t, HipoLogRepayment, got) + require.IsType(t, HipoRepaymentLog{}, value) + }) + t.Run("reuses the decoded body", func(t *testing.T) { + decoded := HipoRepaymentLog{RoundSince: 42} + reused := msg + reused.Body = []byte{1, 2, 3} + reused.DecodedBody = &DecodedMessageBody{Operation: "HipoFinanceLogRepayment", Value: decoded} + _, value, ok := DecodeHipoLog(reused) + require.True(t, ok) + require.Equal(t, decoded, value) + }) + t.Run("a body that does not fit is refused", func(t *testing.T) { + unknownTime := msg + unknownTime.CreatedAt = 0 + _, _, ok := DecodeHipoLog(unknownTime) + require.False(t, ok) + }) +} + +func mustHex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s) + require.NoError(t, err) + return b +} diff --git a/pkg/oas/oas_json_gen.go b/pkg/oas/oas_json_gen.go index c5cc73469..3f2c4c25c 100644 --- a/pkg/oas/oas_json_gen.go +++ b/pkg/oas/oas_json_gen.go @@ -2038,6 +2038,30 @@ func (s *Action) encodeFields(e *jx.Encoder) { s.BuyXTR.Encode(e) } } + { + if s.LoanRequest.Set { + e.FieldStart("LoanRequest") + s.LoanRequest.Encode(e) + } + } + { + if s.LoanIssue.Set { + e.FieldStart("LoanIssue") + s.LoanIssue.Encode(e) + } + } + { + if s.LoanRepayment.Set { + e.FieldStart("LoanRepayment") + s.LoanRepayment.Encode(e) + } + } + { + if s.PoolRoundTransition.Set { + e.FieldStart("PoolRoundTransition") + s.PoolRoundTransition.Encode(e) + } + } { e.FieldStart("simple_preview") s.SimplePreview.Encode(e) @@ -2052,7 +2076,7 @@ func (s *Action) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfAction = [36]string{ +var jsonFieldsNameOfAction = [40]string{ 0: "type", 1: "status", 2: "TonTransfer", @@ -2087,8 +2111,12 @@ var jsonFieldsNameOfAction = [36]string{ 31: "WithdrawXTR", 32: "DepositXTR", 33: "BuyXTR", - 34: "simple_preview", - 35: "base_transactions", + 34: "LoanRequest", + 35: "LoanIssue", + 36: "LoanRepayment", + 37: "PoolRoundTransition", + 38: "simple_preview", + 39: "base_transactions", } // Decode decodes Action from json. @@ -2440,8 +2468,48 @@ func (s *Action) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"BuyXTR\"") } + case "LoanRequest": + if err := func() error { + s.LoanRequest.Reset() + if err := s.LoanRequest.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"LoanRequest\"") + } + case "LoanIssue": + if err := func() error { + s.LoanIssue.Reset() + if err := s.LoanIssue.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"LoanIssue\"") + } + case "LoanRepayment": + if err := func() error { + s.LoanRepayment.Reset() + if err := s.LoanRepayment.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"LoanRepayment\"") + } + case "PoolRoundTransition": + if err := func() error { + s.PoolRoundTransition.Reset() + if err := s.PoolRoundTransition.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"PoolRoundTransition\"") + } case "simple_preview": - requiredBitSet[4] |= 1 << 2 + requiredBitSet[4] |= 1 << 6 if err := func() error { if err := s.SimplePreview.Decode(d); err != nil { return err @@ -2451,7 +2519,7 @@ func (s *Action) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"simple_preview\"") } case "base_transactions": - requiredBitSet[4] |= 1 << 3 + requiredBitSet[4] |= 1 << 7 if err := func() error { s.BaseTransactions = make([]string, 0) if err := d.Arr(func(d *jx.Decoder) error { @@ -2484,7 +2552,7 @@ func (s *Action) Decode(d *jx.Decoder) error { 0b00000000, 0b00000000, 0b00000000, - 0b00001100, + 0b11000000, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. @@ -3056,6 +3124,14 @@ func (s *ActionType) Decode(d *jx.Decoder) error { *s = ActionTypeDepositXTR case ActionTypeWithdrawXTR: *s = ActionTypeWithdrawXTR + case ActionTypeLoanRequest: + *s = ActionTypeLoanRequest + case ActionTypeLoanIssue: + *s = ActionTypeLoanIssue + case ActionTypeLoanRepayment: + *s = ActionTypeLoanRepayment + case ActionTypePoolRoundTransition: + *s = ActionTypePoolRoundTransition case ActionTypeUnknown: *s = ActionTypeUnknown default: @@ -29491,6 +29567,716 @@ func (s *LiquidityDepositAction) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode implements json.Marshaler. +func (s *LoanIssueAction) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *LoanIssueAction) encodeFields(e *jx.Encoder) { + { + e.FieldStart("borrower") + s.Borrower.Encode(e) + } + { + e.FieldStart("pool") + s.Pool.Encode(e) + } + { + e.FieldStart("loan") + s.Loan.Encode(e) + } + { + e.FieldStart("round_since") + e.Int64(s.RoundSince) + } + { + e.FieldStart("amount") + e.Int64(s.Amount) + } + { + e.FieldStart("accrued") + e.Int64(s.Accrued) + } + { + e.FieldStart("collateral") + e.Int64(s.Collateral) + } + { + e.FieldStart("min_payment") + e.Int64(s.MinPayment) + } + { + e.FieldStart("implementation") + s.Implementation.Encode(e) + } +} + +var jsonFieldsNameOfLoanIssueAction = [9]string{ + 0: "borrower", + 1: "pool", + 2: "loan", + 3: "round_since", + 4: "amount", + 5: "accrued", + 6: "collateral", + 7: "min_payment", + 8: "implementation", +} + +// Decode decodes LoanIssueAction from json. +func (s *LoanIssueAction) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode LoanIssueAction to nil") + } + var requiredBitSet [2]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "borrower": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Borrower.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"borrower\"") + } + case "pool": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Pool.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pool\"") + } + case "loan": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + if err := s.Loan.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"loan\"") + } + case "round_since": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + v, err := d.Int64() + s.RoundSince = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"round_since\"") + } + case "amount": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + v, err := d.Int64() + s.Amount = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"amount\"") + } + case "accrued": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + v, err := d.Int64() + s.Accrued = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"accrued\"") + } + case "collateral": + requiredBitSet[0] |= 1 << 6 + if err := func() error { + v, err := d.Int64() + s.Collateral = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"collateral\"") + } + case "min_payment": + requiredBitSet[0] |= 1 << 7 + if err := func() error { + v, err := d.Int64() + s.MinPayment = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"min_payment\"") + } + case "implementation": + requiredBitSet[1] |= 1 << 0 + if err := func() error { + if err := s.Implementation.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"implementation\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode LoanIssueAction") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [2]uint8{ + 0b11111111, + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfLoanIssueAction) { + name = jsonFieldsNameOfLoanIssueAction[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *LoanIssueAction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *LoanIssueAction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *LoanRepaymentAction) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *LoanRepaymentAction) encodeFields(e *jx.Encoder) { + { + e.FieldStart("borrower") + s.Borrower.Encode(e) + } + { + e.FieldStart("pool") + s.Pool.Encode(e) + } + { + e.FieldStart("loan") + s.Loan.Encode(e) + } + { + e.FieldStart("round_since") + e.Int64(s.RoundSince) + } + { + e.FieldStart("amount") + e.Int64(s.Amount) + } + { + e.FieldStart("accrued") + e.Int64(s.Accrued) + } + { + e.FieldStart("recovered") + e.Int64(s.Recovered) + } + { + e.FieldStart("pool_reward") + e.Int64(s.PoolReward) + } + { + e.FieldStart("protocol_fee") + e.Int64(s.ProtocolFee) + } + { + e.FieldStart("borrower_payout") + e.Int64(s.BorrowerPayout) + } + { + e.FieldStart("burn_amount") + e.Int64(s.BurnAmount) + } + { + if s.Shortfall.Set { + e.FieldStart("shortfall") + s.Shortfall.Encode(e) + } + } + { + e.FieldStart("implementation") + s.Implementation.Encode(e) + } +} + +var jsonFieldsNameOfLoanRepaymentAction = [13]string{ + 0: "borrower", + 1: "pool", + 2: "loan", + 3: "round_since", + 4: "amount", + 5: "accrued", + 6: "recovered", + 7: "pool_reward", + 8: "protocol_fee", + 9: "borrower_payout", + 10: "burn_amount", + 11: "shortfall", + 12: "implementation", +} + +// Decode decodes LoanRepaymentAction from json. +func (s *LoanRepaymentAction) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode LoanRepaymentAction to nil") + } + var requiredBitSet [2]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "borrower": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Borrower.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"borrower\"") + } + case "pool": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Pool.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pool\"") + } + case "loan": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + if err := s.Loan.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"loan\"") + } + case "round_since": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + v, err := d.Int64() + s.RoundSince = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"round_since\"") + } + case "amount": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + v, err := d.Int64() + s.Amount = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"amount\"") + } + case "accrued": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + v, err := d.Int64() + s.Accrued = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"accrued\"") + } + case "recovered": + requiredBitSet[0] |= 1 << 6 + if err := func() error { + v, err := d.Int64() + s.Recovered = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"recovered\"") + } + case "pool_reward": + requiredBitSet[0] |= 1 << 7 + if err := func() error { + v, err := d.Int64() + s.PoolReward = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pool_reward\"") + } + case "protocol_fee": + requiredBitSet[1] |= 1 << 0 + if err := func() error { + v, err := d.Int64() + s.ProtocolFee = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"protocol_fee\"") + } + case "borrower_payout": + requiredBitSet[1] |= 1 << 1 + if err := func() error { + v, err := d.Int64() + s.BorrowerPayout = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"borrower_payout\"") + } + case "burn_amount": + requiredBitSet[1] |= 1 << 2 + if err := func() error { + v, err := d.Int64() + s.BurnAmount = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"burn_amount\"") + } + case "shortfall": + if err := func() error { + s.Shortfall.Reset() + if err := s.Shortfall.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"shortfall\"") + } + case "implementation": + requiredBitSet[1] |= 1 << 4 + if err := func() error { + if err := s.Implementation.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"implementation\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode LoanRepaymentAction") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [2]uint8{ + 0b11111111, + 0b00010111, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfLoanRepaymentAction) { + name = jsonFieldsNameOfLoanRepaymentAction[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *LoanRepaymentAction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *LoanRepaymentAction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *LoanRequestAction) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *LoanRequestAction) encodeFields(e *jx.Encoder) { + { + e.FieldStart("borrower") + s.Borrower.Encode(e) + } + { + e.FieldStart("pool") + s.Pool.Encode(e) + } + { + e.FieldStart("round_since") + e.Int64(s.RoundSince) + } + { + if s.Amount.Set { + e.FieldStart("amount") + s.Amount.Encode(e) + } + } + { + if s.MinPayment.Set { + e.FieldStart("min_payment") + s.MinPayment.Encode(e) + } + } + { + e.FieldStart("ton_attached") + e.Int64(s.TonAttached) + } + { + e.FieldStart("implementation") + s.Implementation.Encode(e) + } +} + +var jsonFieldsNameOfLoanRequestAction = [7]string{ + 0: "borrower", + 1: "pool", + 2: "round_since", + 3: "amount", + 4: "min_payment", + 5: "ton_attached", + 6: "implementation", +} + +// Decode decodes LoanRequestAction from json. +func (s *LoanRequestAction) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode LoanRequestAction to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "borrower": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Borrower.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"borrower\"") + } + case "pool": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Pool.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pool\"") + } + case "round_since": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := d.Int64() + s.RoundSince = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"round_since\"") + } + case "amount": + if err := func() error { + s.Amount.Reset() + if err := s.Amount.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"amount\"") + } + case "min_payment": + if err := func() error { + s.MinPayment.Reset() + if err := s.MinPayment.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"min_payment\"") + } + case "ton_attached": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + v, err := d.Int64() + s.TonAttached = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"ton_attached\"") + } + case "implementation": + requiredBitSet[0] |= 1 << 6 + if err := func() error { + if err := s.Implementation.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"implementation\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode LoanRequestAction") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b01100111, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfLoanRequestAction) { + name = jsonFieldsNameOfLoanRequestAction[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *LoanRequestAction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *LoanRequestAction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *MarketTonRates) Encode(e *jx.Encoder) { e.ObjStart() @@ -38260,6 +39046,105 @@ func (s *OptLiquidityDepositAction) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes LoanIssueAction as json. +func (o OptLoanIssueAction) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes LoanIssueAction from json. +func (o *OptLoanIssueAction) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptLoanIssueAction to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptLoanIssueAction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptLoanIssueAction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes LoanRepaymentAction as json. +func (o OptLoanRepaymentAction) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes LoanRepaymentAction from json. +func (o *OptLoanRepaymentAction) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptLoanRepaymentAction to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptLoanRepaymentAction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptLoanRepaymentAction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes LoanRequestAction as json. +func (o OptLoanRequestAction) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes LoanRequestAction from json. +func (o *OptLoanRequestAction) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptLoanRequestAction to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptLoanRequestAction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptLoanRequestAction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes Message as json. func (o OptMessage) Encode(e *jx.Encoder) { if !o.Set { @@ -38826,6 +39711,39 @@ func (s *OptPictureDNS) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes PoolRoundTransitionAction as json. +func (o OptPoolRoundTransitionAction) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes PoolRoundTransitionAction from json. +func (o *OptPoolRoundTransitionAction) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptPoolRoundTransitionAction to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptPoolRoundTransitionAction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptPoolRoundTransitionAction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes Price as json. func (o OptPrice) Encode(e *jx.Encoder) { if !o.Set { @@ -41034,6 +41952,227 @@ func (s *PoolInfo) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes PoolRoundTransition as json. +func (s PoolRoundTransition) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes PoolRoundTransition from json. +func (s *PoolRoundTransition) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PoolRoundTransition to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch PoolRoundTransition(v) { + case PoolRoundTransitionElection: + *s = PoolRoundTransitionElection + case PoolRoundTransitionBegan: + *s = PoolRoundTransitionBegan + case PoolRoundTransitionValidated: + *s = PoolRoundTransitionValidated + case PoolRoundTransitionEnded: + *s = PoolRoundTransitionEnded + case PoolRoundTransitionSettled: + *s = PoolRoundTransitionSettled + default: + *s = PoolRoundTransition(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s PoolRoundTransition) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PoolRoundTransition) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *PoolRoundTransitionAction) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *PoolRoundTransitionAction) encodeFields(e *jx.Encoder) { + { + e.FieldStart("pool") + s.Pool.Encode(e) + } + { + e.FieldStart("round_since") + e.Int64(s.RoundSince) + } + { + e.FieldStart("transition") + s.Transition.Encode(e) + } + { + if s.TotalStaked.Set { + e.FieldStart("total_staked") + s.TotalStaked.Encode(e) + } + } + { + if s.TotalRecovered.Set { + e.FieldStart("total_recovered") + s.TotalRecovered.Encode(e) + } + } + { + e.FieldStart("implementation") + s.Implementation.Encode(e) + } +} + +var jsonFieldsNameOfPoolRoundTransitionAction = [6]string{ + 0: "pool", + 1: "round_since", + 2: "transition", + 3: "total_staked", + 4: "total_recovered", + 5: "implementation", +} + +// Decode decodes PoolRoundTransitionAction from json. +func (s *PoolRoundTransitionAction) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PoolRoundTransitionAction to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "pool": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Pool.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pool\"") + } + case "round_since": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Int64() + s.RoundSince = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"round_since\"") + } + case "transition": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + if err := s.Transition.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"transition\"") + } + case "total_staked": + if err := func() error { + s.TotalStaked.Reset() + if err := s.TotalStaked.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"total_staked\"") + } + case "total_recovered": + if err := func() error { + s.TotalRecovered.Reset() + if err := s.TotalRecovered.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"total_recovered\"") + } + case "implementation": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + if err := s.Implementation.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"implementation\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode PoolRoundTransitionAction") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00100111, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfPoolRoundTransitionAction) { + name = jsonFieldsNameOfPoolRoundTransitionAction[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PoolRoundTransitionAction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PoolRoundTransitionAction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *Price) Encode(e *jx.Encoder) { e.ObjStart() diff --git a/pkg/oas/oas_schemas_gen.go b/pkg/oas/oas_schemas_gen.go index 7b7fedff9..c0fc7cc99 100644 --- a/pkg/oas/oas_schemas_gen.go +++ b/pkg/oas/oas_schemas_gen.go @@ -743,6 +743,10 @@ type Action struct { WithdrawXTR OptWithdrawXTRAction `json:"WithdrawXTR"` DepositXTR OptDepositXTRAction `json:"DepositXTR"` BuyXTR OptBuyXTRAction `json:"BuyXTR"` + LoanRequest OptLoanRequestAction `json:"LoanRequest"` + LoanIssue OptLoanIssueAction `json:"LoanIssue"` + LoanRepayment OptLoanRepaymentAction `json:"LoanRepayment"` + PoolRoundTransition OptPoolRoundTransitionAction `json:"PoolRoundTransition"` SimplePreview ActionSimplePreview `json:"simple_preview"` BaseTransactions []string `json:"base_transactions"` } @@ -917,6 +921,26 @@ func (s *Action) GetBuyXTR() OptBuyXTRAction { return s.BuyXTR } +// GetLoanRequest returns the value of LoanRequest. +func (s *Action) GetLoanRequest() OptLoanRequestAction { + return s.LoanRequest +} + +// GetLoanIssue returns the value of LoanIssue. +func (s *Action) GetLoanIssue() OptLoanIssueAction { + return s.LoanIssue +} + +// GetLoanRepayment returns the value of LoanRepayment. +func (s *Action) GetLoanRepayment() OptLoanRepaymentAction { + return s.LoanRepayment +} + +// GetPoolRoundTransition returns the value of PoolRoundTransition. +func (s *Action) GetPoolRoundTransition() OptPoolRoundTransitionAction { + return s.PoolRoundTransition +} + // GetSimplePreview returns the value of SimplePreview. func (s *Action) GetSimplePreview() ActionSimplePreview { return s.SimplePreview @@ -1097,6 +1121,26 @@ func (s *Action) SetBuyXTR(val OptBuyXTRAction) { s.BuyXTR = val } +// SetLoanRequest sets the value of LoanRequest. +func (s *Action) SetLoanRequest(val OptLoanRequestAction) { + s.LoanRequest = val +} + +// SetLoanIssue sets the value of LoanIssue. +func (s *Action) SetLoanIssue(val OptLoanIssueAction) { + s.LoanIssue = val +} + +// SetLoanRepayment sets the value of LoanRepayment. +func (s *Action) SetLoanRepayment(val OptLoanRepaymentAction) { + s.LoanRepayment = val +} + +// SetPoolRoundTransition sets the value of PoolRoundTransition. +func (s *Action) SetPoolRoundTransition(val OptPoolRoundTransitionAction) { + s.PoolRoundTransition = val +} + // SetSimplePreview sets the value of SimplePreview. func (s *Action) SetSimplePreview(val ActionSimplePreview) { s.SimplePreview = val @@ -1349,6 +1393,10 @@ const ( ActionTypeBuyXTR ActionType = "BuyXTR" ActionTypeDepositXTR ActionType = "DepositXTR" ActionTypeWithdrawXTR ActionType = "WithdrawXTR" + ActionTypeLoanRequest ActionType = "LoanRequest" + ActionTypeLoanIssue ActionType = "LoanIssue" + ActionTypeLoanRepayment ActionType = "LoanRepayment" + ActionTypePoolRoundTransition ActionType = "PoolRoundTransition" ActionTypeUnknown ActionType = "Unknown" ) @@ -1387,6 +1435,10 @@ func (ActionType) AllValues() []ActionType { ActionTypeBuyXTR, ActionTypeDepositXTR, ActionTypeWithdrawXTR, + ActionTypeLoanRequest, + ActionTypeLoanIssue, + ActionTypeLoanRepayment, + ActionTypePoolRoundTransition, ActionTypeUnknown, } } @@ -1458,6 +1510,14 @@ func (s ActionType) MarshalText() ([]byte, error) { return []byte(s), nil case ActionTypeWithdrawXTR: return []byte(s), nil + case ActionTypeLoanRequest: + return []byte(s), nil + case ActionTypeLoanIssue: + return []byte(s), nil + case ActionTypeLoanRepayment: + return []byte(s), nil + case ActionTypePoolRoundTransition: + return []byte(s), nil case ActionTypeUnknown: return []byte(s), nil default: @@ -1564,6 +1624,18 @@ func (s *ActionType) UnmarshalText(data []byte) error { case ActionTypeWithdrawXTR: *s = ActionTypeWithdrawXTR return nil + case ActionTypeLoanRequest: + *s = ActionTypeLoanRequest + return nil + case ActionTypeLoanIssue: + *s = ActionTypeLoanIssue + return nil + case ActionTypeLoanRepayment: + *s = ActionTypeLoanRepayment + return nil + case ActionTypePoolRoundTransition: + *s = ActionTypePoolRoundTransition + return nil case ActionTypeUnknown: *s = ActionTypeUnknown return nil @@ -10214,6 +10286,362 @@ func (s *LiquidityDepositAction) SetTokens(val []VaultDepositInfo) { s.Tokens = val } +// A staking pool lending to a validator, which stakes the loan with the elector. +// Ref: #/components/schemas/LoanIssueAction +type LoanIssueAction struct { + Borrower AccountAddress `json:"borrower"` + Pool AccountAddress `json:"pool"` + // Contract that holds the loan and stakes it. + Loan AccountAddress `json:"loan"` + // Start of the validation round the loan is for, as its unix time. + RoundSince int64 `json:"round_since"` + // Loan granted, in nanograms. + Amount int64 `json:"amount"` + // Pool coins added to the loan beyond what was asked for, in nanograms. + Accrued int64 `json:"accrued"` + // The borrower's own coins staked alongside the loan, in nanograms. + Collateral int64 `json:"collateral"` + // Least the borrower pays the pool from the round's reward, in nanograms. + MinPayment int64 `json:"min_payment"` + Implementation PoolImplementationType `json:"implementation"` +} + +// GetBorrower returns the value of Borrower. +func (s *LoanIssueAction) GetBorrower() AccountAddress { + return s.Borrower +} + +// GetPool returns the value of Pool. +func (s *LoanIssueAction) GetPool() AccountAddress { + return s.Pool +} + +// GetLoan returns the value of Loan. +func (s *LoanIssueAction) GetLoan() AccountAddress { + return s.Loan +} + +// GetRoundSince returns the value of RoundSince. +func (s *LoanIssueAction) GetRoundSince() int64 { + return s.RoundSince +} + +// GetAmount returns the value of Amount. +func (s *LoanIssueAction) GetAmount() int64 { + return s.Amount +} + +// GetAccrued returns the value of Accrued. +func (s *LoanIssueAction) GetAccrued() int64 { + return s.Accrued +} + +// GetCollateral returns the value of Collateral. +func (s *LoanIssueAction) GetCollateral() int64 { + return s.Collateral +} + +// GetMinPayment returns the value of MinPayment. +func (s *LoanIssueAction) GetMinPayment() int64 { + return s.MinPayment +} + +// GetImplementation returns the value of Implementation. +func (s *LoanIssueAction) GetImplementation() PoolImplementationType { + return s.Implementation +} + +// SetBorrower sets the value of Borrower. +func (s *LoanIssueAction) SetBorrower(val AccountAddress) { + s.Borrower = val +} + +// SetPool sets the value of Pool. +func (s *LoanIssueAction) SetPool(val AccountAddress) { + s.Pool = val +} + +// SetLoan sets the value of Loan. +func (s *LoanIssueAction) SetLoan(val AccountAddress) { + s.Loan = val +} + +// SetRoundSince sets the value of RoundSince. +func (s *LoanIssueAction) SetRoundSince(val int64) { + s.RoundSince = val +} + +// SetAmount sets the value of Amount. +func (s *LoanIssueAction) SetAmount(val int64) { + s.Amount = val +} + +// SetAccrued sets the value of Accrued. +func (s *LoanIssueAction) SetAccrued(val int64) { + s.Accrued = val +} + +// SetCollateral sets the value of Collateral. +func (s *LoanIssueAction) SetCollateral(val int64) { + s.Collateral = val +} + +// SetMinPayment sets the value of MinPayment. +func (s *LoanIssueAction) SetMinPayment(val int64) { + s.MinPayment = val +} + +// SetImplementation sets the value of Implementation. +func (s *LoanIssueAction) SetImplementation(val PoolImplementationType) { + s.Implementation = val +} + +// A loan coming back from the elector and its reward being split. +// Ref: #/components/schemas/LoanRepaymentAction +type LoanRepaymentAction struct { + Borrower AccountAddress `json:"borrower"` + Pool AccountAddress `json:"pool"` + Loan AccountAddress `json:"loan"` + // Start of the validation round the loan is for, as its unix time. + RoundSince int64 `json:"round_since"` + // Loan that was granted, in nanograms. + Amount int64 `json:"amount"` + // Pool coins added to the loan beyond what was asked for, in nanograms. + Accrued int64 `json:"accrued"` + // What came back to the pool: the stake and the reward, less what the loan contract spent to forward + // it, in nanograms. + Recovered int64 `json:"recovered"` + // What the pool's stakers earned, in nanograms. + PoolReward int64 `json:"pool_reward"` + // The pool operator's share of the pool's reward, in nanograms; the operator may also receive what + // is left over from fees. + ProtocolFee int64 `json:"protocol_fee"` + // Returned to the borrower: collateral and the borrower's reward, in nanograms. + BorrowerPayout int64 `json:"borrower_payout"` + // Taken from the borrower's reward to be burned, in nanograms. + BurnAmount int64 `json:"burn_amount"` + // Part of the loan the elector did not return and the collateral could not cover, in nanograms. + Shortfall OptInt64 `json:"shortfall"` + Implementation PoolImplementationType `json:"implementation"` +} + +// GetBorrower returns the value of Borrower. +func (s *LoanRepaymentAction) GetBorrower() AccountAddress { + return s.Borrower +} + +// GetPool returns the value of Pool. +func (s *LoanRepaymentAction) GetPool() AccountAddress { + return s.Pool +} + +// GetLoan returns the value of Loan. +func (s *LoanRepaymentAction) GetLoan() AccountAddress { + return s.Loan +} + +// GetRoundSince returns the value of RoundSince. +func (s *LoanRepaymentAction) GetRoundSince() int64 { + return s.RoundSince +} + +// GetAmount returns the value of Amount. +func (s *LoanRepaymentAction) GetAmount() int64 { + return s.Amount +} + +// GetAccrued returns the value of Accrued. +func (s *LoanRepaymentAction) GetAccrued() int64 { + return s.Accrued +} + +// GetRecovered returns the value of Recovered. +func (s *LoanRepaymentAction) GetRecovered() int64 { + return s.Recovered +} + +// GetPoolReward returns the value of PoolReward. +func (s *LoanRepaymentAction) GetPoolReward() int64 { + return s.PoolReward +} + +// GetProtocolFee returns the value of ProtocolFee. +func (s *LoanRepaymentAction) GetProtocolFee() int64 { + return s.ProtocolFee +} + +// GetBorrowerPayout returns the value of BorrowerPayout. +func (s *LoanRepaymentAction) GetBorrowerPayout() int64 { + return s.BorrowerPayout +} + +// GetBurnAmount returns the value of BurnAmount. +func (s *LoanRepaymentAction) GetBurnAmount() int64 { + return s.BurnAmount +} + +// GetShortfall returns the value of Shortfall. +func (s *LoanRepaymentAction) GetShortfall() OptInt64 { + return s.Shortfall +} + +// GetImplementation returns the value of Implementation. +func (s *LoanRepaymentAction) GetImplementation() PoolImplementationType { + return s.Implementation +} + +// SetBorrower sets the value of Borrower. +func (s *LoanRepaymentAction) SetBorrower(val AccountAddress) { + s.Borrower = val +} + +// SetPool sets the value of Pool. +func (s *LoanRepaymentAction) SetPool(val AccountAddress) { + s.Pool = val +} + +// SetLoan sets the value of Loan. +func (s *LoanRepaymentAction) SetLoan(val AccountAddress) { + s.Loan = val +} + +// SetRoundSince sets the value of RoundSince. +func (s *LoanRepaymentAction) SetRoundSince(val int64) { + s.RoundSince = val +} + +// SetAmount sets the value of Amount. +func (s *LoanRepaymentAction) SetAmount(val int64) { + s.Amount = val +} + +// SetAccrued sets the value of Accrued. +func (s *LoanRepaymentAction) SetAccrued(val int64) { + s.Accrued = val +} + +// SetRecovered sets the value of Recovered. +func (s *LoanRepaymentAction) SetRecovered(val int64) { + s.Recovered = val +} + +// SetPoolReward sets the value of PoolReward. +func (s *LoanRepaymentAction) SetPoolReward(val int64) { + s.PoolReward = val +} + +// SetProtocolFee sets the value of ProtocolFee. +func (s *LoanRepaymentAction) SetProtocolFee(val int64) { + s.ProtocolFee = val +} + +// SetBorrowerPayout sets the value of BorrowerPayout. +func (s *LoanRepaymentAction) SetBorrowerPayout(val int64) { + s.BorrowerPayout = val +} + +// SetBurnAmount sets the value of BurnAmount. +func (s *LoanRepaymentAction) SetBurnAmount(val int64) { + s.BurnAmount = val +} + +// SetShortfall sets the value of Shortfall. +func (s *LoanRepaymentAction) SetShortfall(val OptInt64) { + s.Shortfall = val +} + +// SetImplementation sets the value of Implementation. +func (s *LoanRepaymentAction) SetImplementation(val PoolImplementationType) { + s.Implementation = val +} + +// A validator asking a staking pool for a loan to stake in the next validation round. +// Ref: #/components/schemas/LoanRequestAction +type LoanRequestAction struct { + Borrower AccountAddress `json:"borrower"` + Pool AccountAddress `json:"pool"` + // Start of the validation round the loan is for, as its unix time; 0 if the round is unknown. + RoundSince int64 `json:"round_since"` + // Loan asked for, in nanograms; absent if too large to represent. + Amount OptInt64 `json:"amount"` + // Least the borrower promises to pay the pool from the round's reward, in nanograms; absent if too + // large to represent. + MinPayment OptInt64 `json:"min_payment"` + // Sent with the request: the borrower's collateral and the pool's fees, in nanograms. + TonAttached int64 `json:"ton_attached"` + Implementation PoolImplementationType `json:"implementation"` +} + +// GetBorrower returns the value of Borrower. +func (s *LoanRequestAction) GetBorrower() AccountAddress { + return s.Borrower +} + +// GetPool returns the value of Pool. +func (s *LoanRequestAction) GetPool() AccountAddress { + return s.Pool +} + +// GetRoundSince returns the value of RoundSince. +func (s *LoanRequestAction) GetRoundSince() int64 { + return s.RoundSince +} + +// GetAmount returns the value of Amount. +func (s *LoanRequestAction) GetAmount() OptInt64 { + return s.Amount +} + +// GetMinPayment returns the value of MinPayment. +func (s *LoanRequestAction) GetMinPayment() OptInt64 { + return s.MinPayment +} + +// GetTonAttached returns the value of TonAttached. +func (s *LoanRequestAction) GetTonAttached() int64 { + return s.TonAttached +} + +// GetImplementation returns the value of Implementation. +func (s *LoanRequestAction) GetImplementation() PoolImplementationType { + return s.Implementation +} + +// SetBorrower sets the value of Borrower. +func (s *LoanRequestAction) SetBorrower(val AccountAddress) { + s.Borrower = val +} + +// SetPool sets the value of Pool. +func (s *LoanRequestAction) SetPool(val AccountAddress) { + s.Pool = val +} + +// SetRoundSince sets the value of RoundSince. +func (s *LoanRequestAction) SetRoundSince(val int64) { + s.RoundSince = val +} + +// SetAmount sets the value of Amount. +func (s *LoanRequestAction) SetAmount(val OptInt64) { + s.Amount = val +} + +// SetMinPayment sets the value of MinPayment. +func (s *LoanRequestAction) SetMinPayment(val OptInt64) { + s.MinPayment = val +} + +// SetTonAttached sets the value of TonAttached. +func (s *LoanRequestAction) SetTonAttached(val int64) { + s.TonAttached = val +} + +// SetImplementation sets the value of Implementation. +func (s *LoanRequestAction) SetImplementation(val PoolImplementationType) { + s.Implementation = val +} + // Ref: #/components/schemas/MarketTonRates type MarketTonRates struct { Market string `json:"market"` @@ -16646,6 +17074,144 @@ func (o OptLiquidityDepositAction) Or(d LiquidityDepositAction) LiquidityDeposit return d } +// NewOptLoanIssueAction returns new OptLoanIssueAction with value set to v. +func NewOptLoanIssueAction(v LoanIssueAction) OptLoanIssueAction { + return OptLoanIssueAction{ + Value: v, + Set: true, + } +} + +// OptLoanIssueAction is optional LoanIssueAction. +type OptLoanIssueAction struct { + Value LoanIssueAction + Set bool +} + +// IsSet returns true if OptLoanIssueAction was set. +func (o OptLoanIssueAction) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptLoanIssueAction) Reset() { + var v LoanIssueAction + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptLoanIssueAction) SetTo(v LoanIssueAction) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptLoanIssueAction) Get() (v LoanIssueAction, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptLoanIssueAction) Or(d LoanIssueAction) LoanIssueAction { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptLoanRepaymentAction returns new OptLoanRepaymentAction with value set to v. +func NewOptLoanRepaymentAction(v LoanRepaymentAction) OptLoanRepaymentAction { + return OptLoanRepaymentAction{ + Value: v, + Set: true, + } +} + +// OptLoanRepaymentAction is optional LoanRepaymentAction. +type OptLoanRepaymentAction struct { + Value LoanRepaymentAction + Set bool +} + +// IsSet returns true if OptLoanRepaymentAction was set. +func (o OptLoanRepaymentAction) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptLoanRepaymentAction) Reset() { + var v LoanRepaymentAction + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptLoanRepaymentAction) SetTo(v LoanRepaymentAction) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptLoanRepaymentAction) Get() (v LoanRepaymentAction, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptLoanRepaymentAction) Or(d LoanRepaymentAction) LoanRepaymentAction { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptLoanRequestAction returns new OptLoanRequestAction with value set to v. +func NewOptLoanRequestAction(v LoanRequestAction) OptLoanRequestAction { + return OptLoanRequestAction{ + Value: v, + Set: true, + } +} + +// OptLoanRequestAction is optional LoanRequestAction. +type OptLoanRequestAction struct { + Value LoanRequestAction + Set bool +} + +// IsSet returns true if OptLoanRequestAction was set. +func (o OptLoanRequestAction) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptLoanRequestAction) Reset() { + var v LoanRequestAction + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptLoanRequestAction) SetTo(v LoanRequestAction) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptLoanRequestAction) Get() (v LoanRequestAction, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptLoanRequestAction) Or(d LoanRequestAction) LoanRequestAction { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptMessage returns new OptMessage with value set to v. func NewOptMessage(v Message) OptMessage { return OptMessage{ @@ -17416,6 +17982,52 @@ func (o OptPictureDNS) Or(d PictureDNS) PictureDNS { return d } +// NewOptPoolRoundTransitionAction returns new OptPoolRoundTransitionAction with value set to v. +func NewOptPoolRoundTransitionAction(v PoolRoundTransitionAction) OptPoolRoundTransitionAction { + return OptPoolRoundTransitionAction{ + Value: v, + Set: true, + } +} + +// OptPoolRoundTransitionAction is optional PoolRoundTransitionAction. +type OptPoolRoundTransitionAction struct { + Value PoolRoundTransitionAction + Set bool +} + +// IsSet returns true if OptPoolRoundTransitionAction was set. +func (o OptPoolRoundTransitionAction) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptPoolRoundTransitionAction) Reset() { + var v PoolRoundTransitionAction + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptPoolRoundTransitionAction) SetTo(v PoolRoundTransitionAction) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptPoolRoundTransitionAction) Get() (v PoolRoundTransitionAction, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptPoolRoundTransitionAction) Or(d PoolRoundTransitionAction) PoolRoundTransitionAction { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptPrice returns new OptPrice with value set to v. func NewOptPrice(v Price) OptPrice { return OptPrice{ @@ -19267,6 +19879,148 @@ func (s *PoolInfo) SetCycleLength(val OptInt64) { s.CycleLength = val } +// Election: the pool decided which loans to make and staked them with the elector; began: the +// validation round started; validated: the round's validators finished validating and the stakes are +// held until the elector releases them; ended: the round's stakes were released and the pool is +// recovering them; settled: every loan was recovered and the round's deposits and withdrawals were +// settled. +// Ref: #/components/schemas/PoolRoundTransition +type PoolRoundTransition string + +const ( + PoolRoundTransitionElection PoolRoundTransition = "election" + PoolRoundTransitionBegan PoolRoundTransition = "began" + PoolRoundTransitionValidated PoolRoundTransition = "validated" + PoolRoundTransitionEnded PoolRoundTransition = "ended" + PoolRoundTransitionSettled PoolRoundTransition = "settled" +) + +// AllValues returns all PoolRoundTransition values. +func (PoolRoundTransition) AllValues() []PoolRoundTransition { + return []PoolRoundTransition{ + PoolRoundTransitionElection, + PoolRoundTransitionBegan, + PoolRoundTransitionValidated, + PoolRoundTransitionEnded, + PoolRoundTransitionSettled, + } +} + +// MarshalText implements encoding.TextMarshaler. +func (s PoolRoundTransition) MarshalText() ([]byte, error) { + switch s { + case PoolRoundTransitionElection: + return []byte(s), nil + case PoolRoundTransitionBegan: + return []byte(s), nil + case PoolRoundTransitionValidated: + return []byte(s), nil + case PoolRoundTransitionEnded: + return []byte(s), nil + case PoolRoundTransitionSettled: + return []byte(s), nil + default: + return nil, errors.Errorf("invalid value: %q", s) + } +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (s *PoolRoundTransition) UnmarshalText(data []byte) error { + switch PoolRoundTransition(data) { + case PoolRoundTransitionElection: + *s = PoolRoundTransitionElection + return nil + case PoolRoundTransitionBegan: + *s = PoolRoundTransitionBegan + return nil + case PoolRoundTransitionValidated: + *s = PoolRoundTransitionValidated + return nil + case PoolRoundTransitionEnded: + *s = PoolRoundTransitionEnded + return nil + case PoolRoundTransitionSettled: + *s = PoolRoundTransitionSettled + return nil + default: + return errors.Errorf("invalid value: %q", data) + } +} + +// A staking pool moving one of its validation rounds to its next stage. +// Ref: #/components/schemas/PoolRoundTransitionAction +type PoolRoundTransitionAction struct { + Pool AccountAddress `json:"pool"` + // Start of the validation round, as its unix time; 0 if the round is unknown. + RoundSince int64 `json:"round_since"` + Transition PoolRoundTransition `json:"transition"` + // Settled only: lent out for the round, in nanograms. + TotalStaked OptInt64 `json:"total_staked"` + // Settled only: recovered from the round, in nanograms. + TotalRecovered OptInt64 `json:"total_recovered"` + Implementation PoolImplementationType `json:"implementation"` +} + +// GetPool returns the value of Pool. +func (s *PoolRoundTransitionAction) GetPool() AccountAddress { + return s.Pool +} + +// GetRoundSince returns the value of RoundSince. +func (s *PoolRoundTransitionAction) GetRoundSince() int64 { + return s.RoundSince +} + +// GetTransition returns the value of Transition. +func (s *PoolRoundTransitionAction) GetTransition() PoolRoundTransition { + return s.Transition +} + +// GetTotalStaked returns the value of TotalStaked. +func (s *PoolRoundTransitionAction) GetTotalStaked() OptInt64 { + return s.TotalStaked +} + +// GetTotalRecovered returns the value of TotalRecovered. +func (s *PoolRoundTransitionAction) GetTotalRecovered() OptInt64 { + return s.TotalRecovered +} + +// GetImplementation returns the value of Implementation. +func (s *PoolRoundTransitionAction) GetImplementation() PoolImplementationType { + return s.Implementation +} + +// SetPool sets the value of Pool. +func (s *PoolRoundTransitionAction) SetPool(val AccountAddress) { + s.Pool = val +} + +// SetRoundSince sets the value of RoundSince. +func (s *PoolRoundTransitionAction) SetRoundSince(val int64) { + s.RoundSince = val +} + +// SetTransition sets the value of Transition. +func (s *PoolRoundTransitionAction) SetTransition(val PoolRoundTransition) { + s.Transition = val +} + +// SetTotalStaked sets the value of TotalStaked. +func (s *PoolRoundTransitionAction) SetTotalStaked(val OptInt64) { + s.TotalStaked = val +} + +// SetTotalRecovered sets the value of TotalRecovered. +func (s *PoolRoundTransitionAction) SetTotalRecovered(val OptInt64) { + s.TotalRecovered = val +} + +// SetImplementation sets the value of Implementation. +func (s *PoolRoundTransitionAction) SetImplementation(val PoolImplementationType) { + s.Implementation = val +} + // Ref: #/components/schemas/Price type Price struct { CurrencyType CurrencyType `json:"currency_type"` diff --git a/pkg/oas/oas_validators_gen.go b/pkg/oas/oas_validators_gen.go index cba4c42a5..ecd3879c4 100644 --- a/pkg/oas/oas_validators_gen.go +++ b/pkg/oas/oas_validators_gen.go @@ -681,6 +681,78 @@ func (s *Action) Validate() error { Error: err, }) } + if err := func() error { + if value, ok := s.LoanRequest.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "LoanRequest", + Error: err, + }) + } + if err := func() error { + if value, ok := s.LoanIssue.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "LoanIssue", + Error: err, + }) + } + if err := func() error { + if value, ok := s.LoanRepayment.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "LoanRepayment", + Error: err, + }) + } + if err := func() error { + if value, ok := s.PoolRoundTransition.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "PoolRoundTransition", + Error: err, + }) + } if err := func() error { if err := s.SimplePreview.Validate(); err != nil { return err @@ -809,6 +881,14 @@ func (s ActionType) Validate() error { return nil case "WithdrawXTR": return nil + case "LoanRequest": + return nil + case "LoanIssue": + return nil + case "LoanRepayment": + return nil + case "PoolRoundTransition": + return nil case "Unknown": return nil default: @@ -4481,6 +4561,75 @@ func (s *LiquidityDepositAction) Validate() error { return nil } +func (s *LoanIssueAction) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Implementation.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "implementation", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *LoanRepaymentAction) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Implementation.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "implementation", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *LoanRequestAction) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Implementation.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "implementation", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *MarketTonRates) Validate() error { if s == nil { return validate.ErrNilPointer @@ -5669,6 +5818,57 @@ func (s *PoolInfo) Validate() error { return nil } +func (s PoolRoundTransition) Validate() error { + switch s { + case "election": + return nil + case "began": + return nil + case "validated": + return nil + case "ended": + return nil + case "settled": + return nil + default: + return errors.Errorf("invalid value: %v", s) + } +} + +func (s *PoolRoundTransitionAction) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Transition.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "transition", + Error: err, + }) + } + if err := func() error { + if err := s.Implementation.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "implementation", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *Price) Validate() error { if s == nil { return validate.ErrNilPointer diff --git a/pkg/references/hipo.go b/pkg/references/hipo.go index 885c6b7cb..9bad4067b 100644 --- a/pkg/references/hipo.go +++ b/pkg/references/hipo.go @@ -15,4 +15,7 @@ var ( // HipoParent is the jetton master of hGRAM (formerly hTON). It proxies every message // between the treasury and a staker's hGRAM wallet. HipoParent = ton.MustParseAccountID("0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87") + // HipoBurner receives the borrower fee the treasury takes from each loan's reward, + // stakes it, swaps the hGRAM for HPO and burns the HPO. + HipoBurner = ton.MustParseAccountID("0:dc8d90d6be8b68544d17e0749db76a51ddbdac059e178a4cd255274f8aa75030") )