diff --git a/api/openapi.json b/api/openapi.json index ec757345..5756ffcc 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -5225,7 +5225,7 @@ }, "sponsored": { "default": false, - "description": "true — the Battery relay pays gas for this transaction; submit it via /v2/gasless/send. false — self-paid; sign and broadcast via /v2/blockchain/message as usual (e.g. the final TON sweep).\n", + "description": "true — the Battery relay pays gas for this transaction; submit it via /v2/gasless/send. false — self-paid; sign and broadcast via /v2/blockchain/message as usual. The final TON sweep is always self-paid, whatever gas_payer says: the relay does not sponsor a TON-only batch, and gasless has no jetton balance left to bill a commission against.\n", "type": "boolean" }, "state_init": { @@ -5787,6 +5787,10 @@ "trust": { "$ref": "#/components/schemas/TrustType" }, + "trust_v2": { + "$ref": "#/components/schemas/TrustType", + "description": "Corrected trust classification: an item that is neither whitelisted, graylisted, nor blacklisted is TrustNone here (trust still reports TrustBlacklist for that case, for backward compatibility with older clients)." + }, "verified": { "description": "Collection master contract confirmed that this item is part of collection", "example": true, diff --git a/api/openapi.yml b/api/openapi.yml index f73593ac..104820db 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -6628,6 +6628,12 @@ components: example: false trust: $ref: '#/components/schemas/TrustType' + trust_v2: + $ref: '#/components/schemas/TrustType' + description: >- + Corrected trust classification: an item that is neither whitelisted, graylisted, nor + blacklisted is TrustNone here (trust still reports TrustBlacklist for that case, for + backward compatibility with older clients). code_hash: type: string description: Hash of the NFT item account code cell (hex) diff --git a/pkg/api/event_handlers.go b/pkg/api/event_handlers.go index 151df470..091aa817 100644 --- a/pkg/api/event_handlers.go +++ b/pkg/api/event_handlers.go @@ -407,7 +407,7 @@ func (h *Handler) GetAccountEvents(ctx context.Context, params oas.GetAccountEve } for i, e := range events { if e.InProgress { - for j, _ := range e.Actions { + for j := range e.Actions { events[i].Actions[j].Status = oas.ActionStatusOk } } diff --git a/pkg/api/nft_converters.go b/pkg/api/nft_converters.go index 6a0fe585..84c528a5 100644 --- a/pkg/api/nft_converters.go +++ b/pkg/api/nft_converters.go @@ -86,15 +86,24 @@ func (h *Handler) convertNFT(ctx context.Context, item core.NftItem, book addres switch { case len(nftItem.ApprovedBy) > 0 && nftItem.Verified: nftItem.Trust = oas.TrustType(core.TrustWhitelist) + nftItem.TrustV2.SetTo(oas.TrustType(core.TrustWhitelist)) case trustType == core.TrustWhitelist || trustType == core.TrustGraylist: // The item has been reviewed and cleared (support graylisted it, for instance). That // verdict wins over whatever the spam filter's heuristics would otherwise return. nftItem.Trust = oas.TrustType(trustType) + nftItem.TrustV2.SetTo(oas.TrustType(trustType)) default: nftTrust := h.spamFilter.NftTrust(item.Address, item.CollectionAddress, item.OwnerAddress, collectionTrust, name, description, image) if nftTrust == core.TrustNone && trustType != "" { nftTrust = trustType } + nftItem.TrustV2.SetTo(oas.TrustType(nftTrust)) + // trust keeps the historical contract for clients that can't update instantly: an item + // nothing vouches for is blacklisted so they keep blurring it. trust_v2 carries the + // corrected TrustNone value above. + if nftTrust == core.TrustNone { + nftTrust = core.TrustBlacklist + } nftItem.Trust = oas.TrustType(nftTrust) } if image == "" { diff --git a/pkg/api/nft_converters_test.go b/pkg/api/nft_converters_test.go index 1b51a8ca..4f188f81 100644 --- a/pkg/api/nft_converters_test.go +++ b/pkg/api/nft_converters_test.go @@ -26,8 +26,10 @@ func newTestMetaCache(collections map[ton.AccountID]collectionMeta) metadataCach } // TestConvertNFTTrust locks in how the NFT trust sources are combined: an item nothing vouches -// for resolves to TrustNone, an item inherits the trust of its collection, and a review — -// support graylisting the item, or the address book approving it — keeps it visible. +// for is blacklisted in the legacy trust field (so older clients keep blurring it) but TrustNone +// in trust_v2 (the corrected value, for clients that can read it); an item inherits the trust of +// its collection; and a review — support graylisting the item, or the address book approving it — +// keeps it visible in both fields. func TestConvertNFTTrust(t *testing.T) { nftID := ton.MustParseAccountID("EQCNmNR28mDfkwn4bwAlwJ1uhEFnjSQTZ3REz9d7IGZXU9EZ") collectionID := ton.MustParseAccountID("EQDaaxtmY6Dk0YzIV0zNnbUpbjZ92TJHBvO72esc0srwv8K2") @@ -42,46 +44,54 @@ func TestConvertNFTTrust(t *testing.T) { collectionTrust core.TrustType trustType core.TrustType expectedTrust oas.TrustType + expectedTrustV2 oas.TrustType }{ { - name: "NFT without a collection is unreviewed", - collection: nil, - expectedTrust: oas.TrustType(core.TrustNone), + name: "NFT without a collection is unreviewed", + collection: nil, + expectedTrust: oas.TrustType(core.TrustBlacklist), + expectedTrustV2: oas.TrustType(core.TrustNone), }, { - name: "NFT in an unreviewed collection is unreviewed", - collection: &collectionID, - expectedTrust: oas.TrustType(core.TrustNone), + name: "NFT in an unreviewed collection is unreviewed", + collection: &collectionID, + expectedTrust: oas.TrustType(core.TrustBlacklist), + expectedTrustV2: oas.TrustType(core.TrustNone), }, { name: "NFT inherits a blacklisted collection", collection: &collectionID, collectionTrust: core.TrustBlacklist, expectedTrust: oas.TrustType(core.TrustBlacklist), + expectedTrustV2: oas.TrustType(core.TrustBlacklist), }, { name: "NFT inherits a graylisted collection", collection: &collectionID, collectionTrust: core.TrustGraylist, expectedTrust: oas.TrustType(core.TrustGraylist), + expectedTrustV2: oas.TrustType(core.TrustGraylist), }, { - name: "a blacklisted item stays blacklisted", - collection: &collectionID, - trustType: core.TrustBlacklist, - expectedTrust: oas.TrustType(core.TrustBlacklist), + name: "a blacklisted item stays blacklisted", + collection: &collectionID, + trustType: core.TrustBlacklist, + expectedTrust: oas.TrustType(core.TrustBlacklist), + expectedTrustV2: oas.TrustType(core.TrustBlacklist), }, { - name: "a graylisted item stays visible even though nothing else vouches for it", - collection: &collectionID, - trustType: core.TrustGraylist, - expectedTrust: oas.TrustType(core.TrustGraylist), + name: "a graylisted item stays visible even though nothing else vouches for it", + collection: &collectionID, + trustType: core.TrustGraylist, + expectedTrust: oas.TrustType(core.TrustGraylist), + expectedTrustV2: oas.TrustType(core.TrustGraylist), }, { - name: "a whitelisted item stays visible even without a collection", - collection: nil, - trustType: core.TrustWhitelist, - expectedTrust: oas.TrustType(core.TrustWhitelist), + name: "a whitelisted item stays visible even without a collection", + collection: nil, + trustType: core.TrustWhitelist, + expectedTrust: oas.TrustType(core.TrustWhitelist), + expectedTrustV2: oas.TrustType(core.TrustWhitelist), }, } @@ -95,6 +105,9 @@ func TestConvertNFTTrust(t *testing.T) { item := core.NftItem{Address: nftID, CollectionAddress: tt.collection} got := h.convertNFT(context.Background(), item, h.addressBook, h.metaCache, tt.trustType) assert.Equal(t, tt.expectedTrust, got.Trust) + gotTrustV2, ok := got.TrustV2.Get() + assert.True(t, ok, "trust_v2 should always be set") + assert.Equal(t, tt.expectedTrustV2, gotTrustV2) }) } } diff --git a/pkg/bath/intentions.go b/pkg/bath/intentions.go index 67fa6f29..d7a36c36 100644 --- a/pkg/bath/intentions.go +++ b/pkg/bath/intentions.go @@ -6,8 +6,8 @@ import ( "github.com/tonkeeper/tongo" "github.com/tonkeeper/tongo/abi" "github.com/tonkeeper/tongo/boc" - "slices" "reflect" + "slices" ) type OutMessage struct { diff --git a/pkg/bath/staking.go b/pkg/bath/staking.go index daafb898..9eaa85b8 100644 --- a/pkg/bath/staking.go +++ b/pkg/bath/staking.go @@ -361,7 +361,7 @@ var PendingWithdrawRequestLiquidStraw = Straw[BubbleWithdrawStakeRequest]{ return nil }, Children: []Straw[BubbleWithdrawStakeRequest]{ - Straw[BubbleWithdrawStakeRequest]{ + { CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.TonstakePayoutMintJettonsMsgOp)}, SingleChild: &Straw[BubbleWithdrawStakeRequest]{ CheckFuncs: []bubbleCheck{Is(BubbleNftTransfer{})}, @@ -372,7 +372,7 @@ var PendingWithdrawRequestLiquidStraw = Straw[BubbleWithdrawStakeRequest]{ }, Optional: true, }, - Straw[BubbleWithdrawStakeRequest]{ + { CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.TonstakePayoutMintJettonsMsgOp)}, SingleChild: &Straw[BubbleWithdrawStakeRequest]{ CheckFuncs: []bubbleCheck{IsTx, HasOperation(abi.TonstakeNftInitMsgOp)}, diff --git a/pkg/oas/oas_json_gen.go b/pkg/oas/oas_json_gen.go index 7f3cd9a7..64e22366 100644 --- a/pkg/oas/oas_json_gen.go +++ b/pkg/oas/oas_json_gen.go @@ -33772,6 +33772,12 @@ func (s *NftItem) encodeFields(e *jx.Encoder) { e.FieldStart("trust") s.Trust.Encode(e) } + { + if s.TrustV2.Set { + e.FieldStart("trust_v2") + s.TrustV2.Encode(e) + } + } { if s.CodeHash.Set { e.FieldStart("code_hash") @@ -33786,7 +33792,7 @@ func (s *NftItem) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfNftItem = [14]string{ +var jsonFieldsNameOfNftItem = [15]string{ 0: "address", 1: "index", 2: "owner", @@ -33799,8 +33805,9 @@ var jsonFieldsNameOfNftItem = [14]string{ 9: "approved_by", 10: "include_cnft", 11: "trust", - 12: "code_hash", - 13: "data_hash", + 12: "trust_v2", + 13: "code_hash", + 14: "data_hash", } // Decode decodes NftItem from json. @@ -33945,6 +33952,16 @@ func (s *NftItem) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"trust\"") } + case "trust_v2": + if err := func() error { + s.TrustV2.Reset() + if err := s.TrustV2.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"trust_v2\"") + } case "code_hash": if err := func() error { s.CodeHash.Reset() @@ -39279,6 +39296,39 @@ func (s *OptTonTransferAction) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes TrustType as json. +func (o OptTrustType) Encode(e *jx.Encoder) { + if !o.Set { + return + } + e.Str(string(o.Value)) +} + +// Decode decodes TrustType from json. +func (o *OptTrustType) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptTrustType to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptTrustType) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptTrustType) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes uint32 as json. func (o OptUint32) Encode(e *jx.Encoder) { if !o.Set { diff --git a/pkg/oas/oas_schemas_gen.go b/pkg/oas/oas_schemas_gen.go index c3f27b3c..f060fbf7 100644 --- a/pkg/oas/oas_schemas_gen.go +++ b/pkg/oas/oas_schemas_gen.go @@ -10887,7 +10887,9 @@ type MigrationTransaction struct { // body — sign and wrap it for /v2/gasless/send as in the gasless flow. Boc string `json:"boc"` // True — the Battery relay pays gas for this transaction; submit it via /v2/gasless/send. false - // — self-paid; sign and broadcast via /v2/blockchain/message as usual (e.g. the final TON sweep). + // — self-paid; sign and broadcast via /v2/blockchain/message as usual. The final TON sweep is + // always self-paid, whatever gas_payer says: the relay does not sponsor a TON-only batch, and + // gasless has no jetton balance left to bill a commission against. Sponsored OptBool `json:"sponsored"` // Gasless only; the relay commission in indivisible gas-jetton units, embedded in the boc as a // jetton transfer to the relay. Exact for the first transaction; an estimate for later ones @@ -11847,6 +11849,10 @@ type NftItem struct { ApprovedBy NftApprovedBy `json:"approved_by"` IncludeCnft OptBool `json:"include_cnft"` Trust TrustType `json:"trust"` + // Corrected trust classification: an item that is neither whitelisted, graylisted, nor blacklisted + // is TrustNone here (trust still reports TrustBlacklist for that case, for backward compatibility + // with older clients). + TrustV2 OptTrustType `json:"trust_v2"` // Hash of the NFT item account code cell (hex). CodeHash OptString `json:"code_hash"` // Hash of the NFT item account data cell (hex). @@ -11913,6 +11919,11 @@ func (s *NftItem) GetTrust() TrustType { return s.Trust } +// GetTrustV2 returns the value of TrustV2. +func (s *NftItem) GetTrustV2() OptTrustType { + return s.TrustV2 +} + // GetCodeHash returns the value of CodeHash. func (s *NftItem) GetCodeHash() OptString { return s.CodeHash @@ -11983,6 +11994,11 @@ func (s *NftItem) SetTrust(val TrustType) { s.Trust = val } +// SetTrustV2 sets the value of TrustV2. +func (s *NftItem) SetTrustV2(val OptTrustType) { + s.TrustV2 = val +} + // SetCodeHash sets the value of CodeHash. func (s *NftItem) SetCodeHash(val OptString) { s.CodeHash = val @@ -18206,6 +18222,52 @@ func (o OptTonTransferAction) Or(d TonTransferAction) TonTransferAction { return d } +// NewOptTrustType returns new OptTrustType with value set to v. +func NewOptTrustType(v TrustType) OptTrustType { + return OptTrustType{ + Value: v, + Set: true, + } +} + +// OptTrustType is optional TrustType. +type OptTrustType struct { + Value TrustType + Set bool +} + +// IsSet returns true if OptTrustType was set. +func (o OptTrustType) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptTrustType) Reset() { + var v TrustType + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptTrustType) SetTo(v TrustType) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptTrustType) Get() (v TrustType, 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 OptTrustType) Or(d TrustType) TrustType { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptUint32 returns new OptUint32 with value set to v. func NewOptUint32(v uint32) OptUint32 { return OptUint32{ diff --git a/pkg/oas/oas_validators_gen.go b/pkg/oas/oas_validators_gen.go index f55f1260..ae2cfdf6 100644 --- a/pkg/oas/oas_validators_gen.go +++ b/pkg/oas/oas_validators_gen.go @@ -5173,6 +5173,24 @@ func (s *NftItem) Validate() error { Error: err, }) } + if err := func() error { + if value, ok := s.TrustV2.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: "trust_v2", + Error: err, + }) + } if len(failures) > 0 { return &validate.Error{Fields: failures} } diff --git a/pkg/references/protocols.go b/pkg/references/protocols.go index b3f1b13f..69c731f1 100644 --- a/pkg/references/protocols.go +++ b/pkg/references/protocols.go @@ -8,10 +8,10 @@ const ( ) var ( - EthenaImage = "https://ethena.fi/shared/usde.png" - BidaskImage = "https://bidask.finance/assets/landing/bidask-logo.webp" - StonfiImage = "https://static.ston.fi/favicon/android-chrome-192x192.png" - MooncxImage = "https://moon.cx/assets/logoMoon.svg" + EthenaImage = "https://ethena.fi/shared/usde.png" + BidaskImage = "https://bidask.finance/assets/landing/bidask-logo.webp" + StonfiImage = "https://static.ston.fi/favicon/android-chrome-192x192.png" + MooncxImage = "https://moon.cx/assets/logoMoon.svg" ToncoImage = "https://ton.app/media/1f913e65-9c32-433e-a0a3-a7c5ccf46ad5.png" AffluentImage = "https://ton.app/media/71e5021a-77ab-4c48-8ef3-35e9c67701b0.png" ) diff --git a/pkg/wallet/risk_test.go b/pkg/wallet/risk_test.go index e84d97ba..77aad392 100644 --- a/pkg/wallet/risk_test.go +++ b/pkg/wallet/risk_test.go @@ -21,7 +21,7 @@ func TestExtractRisk(t *testing.T) { name: "transfer ton", boc: "te6ccgEBAgEAqgAB4YgA2ZpktQsYby0n9cV5VWOFINBjScIU2HdondFsK3lDpEAAQ+B903cV6YIMdtd4QtdyekehadSk+QjIgoIiRgjZD9v81PVGEXBKHPgPUknVvxvr/LGcKkLNhY+I1Wuwi/7ACU1NGLsi5dhQAAAA8AAcAQBoQgApn5hvK5EKvcI4+qgdz+LABkbBy/PLofvLWI8wTW1zT6WWgvAAAAAAAAAAAAAAAAAAAA==", want: &Risk{ - Gram: 3_000_000_000, + Gram: 3_000_000_000, Jettons: map[tongo.AccountID]big.Int{}, Nfts: nil, }, @@ -41,7 +41,7 @@ func TestExtractRisk(t *testing.T) { boc: "te6ccgECAwEAAQAAAeGIANmaZLULGG8tJ/XFeVVjhSDQY0nCFNh3aJ3RbCt5Q6RAAR/y7WiDk/zi6/QObgK7qDZRawFY0k5TaspQuK98GHfLWcVcMgc/kdpXj+nNrmpWHO2mJ6nyxhuxwzzphZVmuBlNTRi7I88W+AAAARAAHAEBaGIAYeITnAruocV3ZaCBjfbcIK27S8GFMv5jOh6XPwNuAUkgFykzCAAAAAAAAAAAAAAAAAECAKVfzD0UAAAAAAAAAACACmfmG8rkQq9wjj6qB3P4sAGRsHL88uh+8tYjzBNbXNPwAbM0yWoWMN5aT+uK8qrHCkGgxpOEKbDu0Tui2Fbyh0iAcxLQCA==", want: &Risk{ Jettons: map[tongo.AccountID]big.Int{}, - Gram: 48_572_001, + Gram: 48_572_001, Nfts: []tongo.AccountID{ tongo.MustParseAccountID("0:c3c4273815dd438aeecb41031bedb8415b7697830a65fcc6743d2e7e06dc0292"), },