From bdad28beac978f8884e439b4dd52cb9f5939061b Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Fri, 31 Jul 2026 00:55:14 +0100 Subject: [PATCH 1/8] chore: add assets-controller messenger dependencies Co-authored-by: Cursor --- packages/tron-wallet-snap/package.json | 3 + yarn.lock | 1727 +++++++++++++++++++++++- 2 files changed, 1690 insertions(+), 40 deletions(-) diff --git a/packages/tron-wallet-snap/package.json b/packages/tron-wallet-snap/package.json index df2092c4..d806628d 100644 --- a/packages/tron-wallet-snap/package.json +++ b/packages/tron-wallet-snap/package.json @@ -48,10 +48,13 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "devDependencies": { + "@metamask/assets-controller": "11.2.0", "@metamask/auto-changelog": "^6.1.1", "@metamask/key-tree": "^10.1.1", "@metamask/keyring-api": "^23.7.0", "@metamask/keyring-snap-sdk": "^9.2.1", + "@metamask/messenger": "^2.0.0", + "@metamask/remote-feature-flag-controller": "4.2.2", "@metamask/snaps-cli": "^8.4.1", "@metamask/snaps-jest": "^10.2.0", "@metamask/snaps-sdk": "^11.2.0", diff --git a/yarn.lock b/yarn.lock index 5fd773e5..9a9032e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -839,7 +839,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/abi@npm:^5.7.0": +"@ethersproject/abi@npm:^5.7.0, @ethersproject/abi@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/abi@npm:5.8.0" dependencies: @@ -884,7 +884,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/address@npm:^5.8.0": +"@ethersproject/address@npm:^5.7.0, @ethersproject/address@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/address@npm:5.8.0" dependencies: @@ -906,7 +906,17 @@ __metadata: languageName: node linkType: hard -"@ethersproject/bignumber@npm:^5.8.0": +"@ethersproject/basex@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/basex@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + checksum: 10/1a8d48a9397461ea42ec43b69a15a0d13ba0b9192695713750d9d391503c55b258cca435fa78a4014d23a813053f1a471593b89c7c0d89351639a78d50a12ef2 + languageName: node + linkType: hard + +"@ethersproject/bignumber@npm:^5.7.0, @ethersproject/bignumber@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/bignumber@npm:5.8.0" dependencies: @@ -935,6 +945,24 @@ __metadata: languageName: node linkType: hard +"@ethersproject/contracts@npm:^5.7.0": + version: 5.8.0 + resolution: "@ethersproject/contracts@npm:5.8.0" + dependencies: + "@ethersproject/abi": "npm:^5.8.0" + "@ethersproject/abstract-provider": "npm:^5.8.0" + "@ethersproject/abstract-signer": "npm:^5.8.0" + "@ethersproject/address": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/constants": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/transactions": "npm:^5.8.0" + checksum: 10/839f8211f5e560f15468ae843ba316ffeacab5cebcece1eec76bc5714472ebfe3453484f283d3e46b9d3faaffef1e17cc3583cf24e01638a1fd52f69012cf8d4 + languageName: node + linkType: hard + "@ethersproject/hash@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/hash@npm:5.8.0" @@ -952,6 +980,47 @@ __metadata: languageName: node linkType: hard +"@ethersproject/hdnode@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/hdnode@npm:5.8.0" + dependencies: + "@ethersproject/abstract-signer": "npm:^5.8.0" + "@ethersproject/basex": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/pbkdf2": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/sha2": "npm:^5.8.0" + "@ethersproject/signing-key": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + "@ethersproject/transactions": "npm:^5.8.0" + "@ethersproject/wordlists": "npm:^5.8.0" + checksum: 10/55b35cf30f0dd40e2d5ecd4b2f005ebea82a85a440717a61d4a483074f652d2c7063e9c704272b894bfdd500f7883aa36692931c6808591f702c1da7107ebb61 + languageName: node + linkType: hard + +"@ethersproject/json-wallets@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/json-wallets@npm:5.8.0" + dependencies: + "@ethersproject/abstract-signer": "npm:^5.8.0" + "@ethersproject/address": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/hdnode": "npm:^5.8.0" + "@ethersproject/keccak256": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/pbkdf2": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/random": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + "@ethersproject/transactions": "npm:^5.8.0" + aes-js: "npm:3.0.0" + scrypt-js: "npm:3.0.1" + checksum: 10/5cbf7e698ee7f26f54fceb672d9824b01816cd785182e638cb5cd1eaed5d80d8a4576e3cad92af46ac6d23404a806a47a72d5dee908af42322d091553a0d8da6 + languageName: node + linkType: hard + "@ethersproject/keccak256@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/keccak256@npm:5.8.0" @@ -978,6 +1047,16 @@ __metadata: languageName: node linkType: hard +"@ethersproject/pbkdf2@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/pbkdf2@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/sha2": "npm:^5.8.0" + checksum: 10/203bb992eec3042256702f4c8259a37202af7b341cc6e370614cdc52541042fc3b795fb040592bd6be8b67376a798c45312ca1e6d5d179c3e8eb7431882f1fd1 + languageName: node + linkType: hard + "@ethersproject/properties@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/properties@npm:5.8.0" @@ -987,6 +1066,44 @@ __metadata: languageName: node linkType: hard +"@ethersproject/providers@npm:^5.7.0, @ethersproject/providers@npm:^5.7.2": + version: 5.8.0 + resolution: "@ethersproject/providers@npm:5.8.0" + dependencies: + "@ethersproject/abstract-provider": "npm:^5.8.0" + "@ethersproject/abstract-signer": "npm:^5.8.0" + "@ethersproject/address": "npm:^5.8.0" + "@ethersproject/base64": "npm:^5.8.0" + "@ethersproject/basex": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/constants": "npm:^5.8.0" + "@ethersproject/hash": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/networks": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/random": "npm:^5.8.0" + "@ethersproject/rlp": "npm:^5.8.0" + "@ethersproject/sha2": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + "@ethersproject/transactions": "npm:^5.8.0" + "@ethersproject/web": "npm:^5.8.0" + bech32: "npm:1.1.4" + ws: "npm:8.18.0" + checksum: 10/7d40fc0abb78fc9e69b71cb560beb2a93cf1da2cf978a061031a34c0ed76c2f5936ed8c0bdb9aa1307fe5308d0159e429b83b779dbd550639a886a88d6d17817 + languageName: node + linkType: hard + +"@ethersproject/random@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/random@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + checksum: 10/47c34a72c81183ac13a1b4635bb9d5cf1456e6329276f50c9e12711f404a9eb4536db824537ed05ef8839a0a358883dc3342d3ea83147b8bafeb767dc8f57e23 + languageName: node + linkType: hard + "@ethersproject/rlp@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/rlp@npm:5.8.0" @@ -997,6 +1114,17 @@ __metadata: languageName: node linkType: hard +"@ethersproject/sha2@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/sha2@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + hash.js: "npm:1.1.7" + checksum: 10/ef8916e3033502476fba9358ba1993722ac3bb99e756d5681e4effa3dfa0f0bf0c29d3fa338662830660b45dd359cccb06ba40bc7b62cfd44f4a177b25829404 + languageName: node + linkType: hard + "@ethersproject/signing-key@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/signing-key@npm:5.8.0" @@ -1039,6 +1167,29 @@ __metadata: languageName: node linkType: hard +"@ethersproject/wallet@npm:^5.7.0": + version: 5.8.0 + resolution: "@ethersproject/wallet@npm:5.8.0" + dependencies: + "@ethersproject/abstract-provider": "npm:^5.8.0" + "@ethersproject/abstract-signer": "npm:^5.8.0" + "@ethersproject/address": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/hash": "npm:^5.8.0" + "@ethersproject/hdnode": "npm:^5.8.0" + "@ethersproject/json-wallets": "npm:^5.8.0" + "@ethersproject/keccak256": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/random": "npm:^5.8.0" + "@ethersproject/signing-key": "npm:^5.8.0" + "@ethersproject/transactions": "npm:^5.8.0" + "@ethersproject/wordlists": "npm:^5.8.0" + checksum: 10/354c8985a74b1bb0a8ba80f374c1af882f7657716b974dda235184ee98151e30741b24f58a93c84693aa6e72a8a5c3ae62143966967f40f52f62093559388e6a + languageName: node + linkType: hard + "@ethersproject/web@npm:^5.8.0": version: 5.8.0 resolution: "@ethersproject/web@npm:5.8.0" @@ -1052,6 +1203,19 @@ __metadata: languageName: node linkType: hard +"@ethersproject/wordlists@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/wordlists@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/hash": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + checksum: 10/b8e6aa7d2195bb568847f360f6525ddc3d145404fbd4553e2e05daf4a95f58167591feb69e16e3398a28114ea85e1895fc8f5bd1c0cbf8b578123d7c1d21c32d + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.1": version: 0.19.1 resolution: "@humanfs/core@npm:0.19.1" @@ -1690,6 +1854,90 @@ __metadata: languageName: node linkType: hard +"@metamask/abi-utils@npm:^2.0.3": + version: 2.0.4 + resolution: "@metamask/abi-utils@npm:2.0.4" + dependencies: + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^9.0.0" + checksum: 10/3d32d42c6e98fc4719b2b53597e573764b80936c7cc31d884c87729c4c4f74a30e93096db87aaa7cbcec9d3bb7d22b1adfc98a8bcb4c7c2f17bfbddaa4367d34 + languageName: node + linkType: hard + +"@metamask/abi-utils@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/abi-utils@npm:3.0.0" + dependencies: + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.0.1" + checksum: 10/068b98185148b9e185b4af4392c6a6f82f1d4b1ff60013c57679c618f37afe9030e3ccc940e1a8b690be6f62ea91115ab18b73f3c3c09f4eff1794e31ababb9b + languageName: node + linkType: hard + +"@metamask/account-api@npm:^1.1.1": + version: 1.1.1 + resolution: "@metamask/account-api@npm:1.1.1" + dependencies: + "@metamask/keyring-api": "npm:^23.6.0" + "@metamask/keyring-utils": "npm:^4.0.0" + uuid: "npm:^9.0.1" + checksum: 10/2991dbfbbbe12437c2267c8be7f52f2eea8f6c3a021bcc21c30a07506a02894b207f29775be5e4e81a7453fc23dbafe94013fe1f48a3d6044d4527f95ae91827 + languageName: node + linkType: hard + +"@metamask/account-tree-controller@npm:^7.5.5": + version: 7.5.5 + resolution: "@metamask/account-tree-controller@npm:7.5.5" + dependencies: + "@metamask/accounts-controller": "npm:^39.0.5" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/keyring-api": "npm:^23.5.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/multichain-account-service": "npm:^13.0.0" + "@metamask/profile-sync-controller": "npm:^28.3.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.11.0" + fast-deep-equal: "npm:^3.1.3" + lodash: "npm:^4.17.21" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/18913a9cdf65a5ab640d2cb608f6fb46b0f4ebce1537d4ed3e6502392efb15acb45b0648319a861421e6f527a40c7574e9e505fc4cefdc4f9fde0a06df1e428e + languageName: node + linkType: hard + +"@metamask/accounts-controller@npm:^39.0.4, @metamask/accounts-controller@npm:^39.0.5": + version: 39.0.5 + resolution: "@metamask/accounts-controller@npm:39.0.5" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/eth-snap-keyring": "npm:^23.0.0" + "@metamask/keyring-api": "npm:^23.5.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/keyring-sdk": "npm:^2.2.0" + "@metamask/keyring-utils": "npm:^3.3.1" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.11.0" + deepmerge: "npm:^4.2.2" + ethereum-cryptography: "npm:^2.1.2" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/5d505931fa24d9d091b4822fe3d640d712b0a65d97886ca4b50b7842613326e160d6b48ac23dcf5ac5a1507bf4c8207748d238fca1998ac90e86f5eaa02f74fc + languageName: node + linkType: hard + "@metamask/action-utils@npm:^1.0.0": version: 1.1.1 resolution: "@metamask/action-utils@npm:1.1.1" @@ -1701,6 +1949,18 @@ __metadata: languageName: node linkType: hard +"@metamask/address-book-controller@npm:^7.1.2": + version: 7.1.2 + resolution: "@metamask/address-book-controller@npm:7.1.2" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/utils": "npm:^11.9.0" + checksum: 10/e15140418452a80d51fec783003144add72a50a4fe362377481de83c7858860f641a3cfb1575f4b802b1eb1e873b3efd03bb4183afcbee77f95c7525e05aaed4 + languageName: node + linkType: hard + "@metamask/analytics-controller@npm:^1.1.1": version: 1.2.1 resolution: "@metamask/analytics-controller@npm:1.2.1" @@ -1734,6 +1994,101 @@ __metadata: languageName: node linkType: hard +"@metamask/assets-controller@npm:11.2.0": + version: 11.2.0 + resolution: "@metamask/assets-controller@npm:11.2.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/account-tree-controller": "npm:^7.5.5" + "@metamask/accounts-controller": "npm:^39.0.5" + "@metamask/assets-controllers": "npm:^110.0.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/client-controller": "npm:^1.0.1" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/core-backend": "npm:^7.0.0" + "@metamask/keyring-api": "npm:^23.5.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/keyring-snap-client": "npm:^9.2.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/network-enablement-controller": "npm:^5.6.0" + "@metamask/permission-controller": "npm:^13.1.1" + "@metamask/phishing-controller": "npm:^17.3.0" + "@metamask/polling-controller": "npm:^16.0.8" + "@metamask/preferences-controller": "npm:^23.1.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.2" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/transaction-controller": "npm:^69.2.1" + "@metamask/utils": "npm:^11.11.0" + async-mutex: "npm:^0.5.0" + bignumber.js: "npm:^9.1.2" + lodash: "npm:^4.17.21" + p-limit: "npm:^3.1.0" + checksum: 10/45f420415b569badbbbf36c25851a4e658b45c904ff11411bb610fdc5e258e40fbded13c9f413834f5d38ce2e7aa31449490c8e925c3bf5bc2c7ae6c7c42f675 + languageName: node + linkType: hard + +"@metamask/assets-controllers@npm:^110.0.0": + version: 110.0.2 + resolution: "@metamask/assets-controllers@npm:110.0.2" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/account-tree-controller": "npm:^7.5.5" + "@metamask/accounts-controller": "npm:^39.0.5" + "@metamask/approval-controller": "npm:^9.0.2" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/core-backend": "npm:^8.0.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/keyring-api": "npm:^23.7.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-account-service": "npm:^13.0.0" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/network-enablement-controller": "npm:^6.0.0" + "@metamask/permission-controller": "npm:^13.1.1" + "@metamask/phishing-controller": "npm:^17.3.0" + "@metamask/polling-controller": "npm:^16.0.8" + "@metamask/preferences-controller": "npm:^23.1.0" + "@metamask/profile-sync-controller": "npm:^28.3.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/storage-service": "npm:^1.0.2" + "@metamask/transaction-controller": "npm:^69.3.0" + "@metamask/utils": "npm:^11.11.0" + "@tanstack/query-core": "npm:^5.62.16" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + bn.js: "npm:^5.2.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^9.9.0" + reselect: "npm:^5.1.1" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/b1ba364105db912eeb3a1dbe43c11bf0c8af7ce0c5f685d09d3f4cdeade9d99d3e5f6e4d2f9b62901fdb670e6adc3fcd9babfd296ed512ffbceac080f3950484 + languageName: node + linkType: hard + "@metamask/auto-changelog@npm:^6.1.1": version: 6.1.1 resolution: "@metamask/auto-changelog@npm:6.1.1" @@ -1806,6 +2161,15 @@ __metadata: languageName: node linkType: hard +"@metamask/browser-passworder@npm:^6.0.0": + version: 6.0.0 + resolution: "@metamask/browser-passworder@npm:6.0.0" + dependencies: + "@metamask/utils": "npm:^11.0.1" + checksum: 10/44b31729ccd52e62df27a948fbea2320a11861d6465128b1626a723fc47e25f9e3a8c3bdc09329f3f270b5d9e023107cb439cfb6961a3614c6daa58ea4524f00 + languageName: node + linkType: hard + "@metamask/chain-agnostic-permission@npm:^1.6.2": version: 1.7.0 resolution: "@metamask/chain-agnostic-permission@npm:1.7.0" @@ -1820,6 +2184,51 @@ __metadata: languageName: node linkType: hard +"@metamask/client-controller@npm:^1.0.1": + version: 1.0.1 + resolution: "@metamask/client-controller@npm:1.0.1" + dependencies: + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/messenger": "npm:^1.0.0" + checksum: 10/7a3db2c30c6217a018a7cc36cb40b0be21ec66d1816327619ca91e843657ea684a2194fd27a6d58cb2f9e6f5736e44277c9eb026e9606dd2ae522be68affc912 + languageName: node + linkType: hard + +"@metamask/config-registry-controller@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/config-registry-controller@npm:1.0.0" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/polling-controller": "npm:^16.0.8" + "@metamask/profile-sync-controller": "npm:^28.3.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.2" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.11.0" + reselect: "npm:^5.1.1" + checksum: 10/d02172b4b58e74e87c76c067c0197eebf20365d4bccb9d5307e6f275dc6bfb7f65f11286493da4cbc26ed513df6be73db7f02929a3d60a231ea2bc0fee877abd + languageName: node + linkType: hard + +"@metamask/connectivity-controller@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask/connectivity-controller@npm:0.2.0" + dependencies: + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/messenger": "npm:^1.0.0" + checksum: 10/586c80931341375c713aa5a474725ec88174e6e885d5f75d23b6458b734ec0e912a8c47996bea94a82f9549c9598ba982ac99aaeccac1fd92002c35a38f5397f + languageName: node + linkType: hard + +"@metamask/contract-metadata@npm:^2.4.0": + version: 2.5.0 + resolution: "@metamask/contract-metadata@npm:2.5.0" + checksum: 10/462b3ec7b32311ff5e6eec001abf25143e67ee667c70e0b9a3dee7545e7fc245b8098856ae79fef9758ef1db4930fa1d1d7681410995520935f2885bdb9641d2 + languageName: node + linkType: hard + "@metamask/controller-utils@npm:^11.19.0": version: 11.20.0 resolution: "@metamask/controller-utils@npm:11.20.0" @@ -1841,7 +2250,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^12.0.0, @metamask/controller-utils@npm:^12.3.0": +"@metamask/controller-utils@npm:^12.0.0, @metamask/controller-utils@npm:^12.1.0, @metamask/controller-utils@npm:^12.3.0": version: 12.3.0 resolution: "@metamask/controller-utils@npm:12.3.0" dependencies: @@ -1863,6 +2272,42 @@ __metadata: languageName: node linkType: hard +"@metamask/core-backend@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/core-backend@npm:7.0.0" + dependencies: + "@metamask/account-tree-controller": "npm:^7.5.5" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/profile-sync-controller": "npm:^28.3.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.2" + "@metamask/utils": "npm:^11.11.0" + "@tanstack/query-core": "npm:^5.62.16" + async-mutex: "npm:^0.5.0" + uuid: "npm:^8.3.2" + checksum: 10/306ccde8856832b236984dcd6ae7044b8664c6319a80f199b23a2cd91883188d921968387bb515defe8d36cd3d06d3f9db0144e821248a844ad1907ca9ec902b + languageName: node + linkType: hard + +"@metamask/core-backend@npm:^8.0.0": + version: 8.0.0 + resolution: "@metamask/core-backend@npm:8.0.0" + dependencies: + "@metamask/account-tree-controller": "npm:^7.5.5" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/profile-sync-controller": "npm:^28.3.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.2" + "@metamask/utils": "npm:^11.11.0" + "@tanstack/query-core": "npm:^5.62.16" + async-mutex: "npm:^0.5.0" + uuid: "npm:^8.3.2" + checksum: 10/5b35e69b72340904f12a2c1f2fd46dbf5006242cfc4470a32379457ffe4a885718c4e20c6555eeb42da814d4f2596988a60549a37216143c076ee4e26a2e28d4 + languageName: node + linkType: hard + "@metamask/create-release-branch@npm:^4.2.1": version: 4.2.1 resolution: "@metamask/create-release-branch@npm:4.2.1" @@ -1957,6 +2402,93 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-block-tracker@npm:^15.0.1": + version: 15.0.1 + resolution: "@metamask/eth-block-tracker@npm:15.0.1" + dependencies: + "@metamask/eth-json-rpc-provider": "npm:^6.0.0" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^11.9.0" + json-rpc-random-id: "npm:^1.0.1" + checksum: 10/8c0e0f5bde00fb40aa240fc562c29d0ce84156402132705d387c95453c6f265b3b738c9b649510771965d8b5b8036c2bdc6406142ff634383bf60df3741bdbfe + languageName: node + linkType: hard + +"@metamask/eth-hd-keyring@npm:^14.1.1": + version: 14.1.2 + resolution: "@metamask/eth-hd-keyring@npm:14.1.2" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/key-tree": "npm:^10.0.2" + "@metamask/keyring-api": "npm:^23.6.0" + "@metamask/keyring-sdk": "npm:^2.3.0" + "@metamask/keyring-utils": "npm:^4.0.0" + "@metamask/scure-bip39": "npm:^2.1.1" + "@metamask/superstruct": "npm:^3.4.1" + "@metamask/utils": "npm:^11.11.0" + ethereum-cryptography: "npm:^2.2.1" + checksum: 10/077d6aacc674903d737948dfb5a3215e912e73afabad14429614ca643269150604b8ded8cbd48601f99b532c07e51c8d6b1036bad304a21a5b2ded3ea7d06c02 + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-infura@npm:^10.3.0": + version: 10.3.0 + resolution: "@metamask/eth-json-rpc-infura@npm:10.3.0" + dependencies: + "@metamask/eth-json-rpc-provider": "npm:^5.0.0" + "@metamask/json-rpc-engine": "npm:^10.1.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.0.1" + checksum: 10/164c9ef1285a0895db668f3949d9258df721d348c857c4337b964f147000221d69fd103851c6483d8dd6bfe851c76cc5299155a8818eac0b92334469c40ccaef + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-middleware@npm:^23.1.3": + version: 23.1.3 + resolution: "@metamask/eth-json-rpc-middleware@npm:23.1.3" + dependencies: + "@metamask/eth-block-tracker": "npm:^15.0.1" + "@metamask/eth-json-rpc-provider": "npm:^6.0.1" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/json-rpc-engine": "npm:^10.2.4" + "@metamask/message-manager": "npm:^14.1.1" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.9.0" + klona: "npm:^2.0.6" + pify: "npm:^5.0.0" + safe-stable-stringify: "npm:^2.4.3" + checksum: 10/aee4866afa78cb21aed9daa1211ee9a3515cb9549d9b325d5904b8769fb54790296224b58ec3555eb34a7125d2fd5180d6cba915bb1ca8dc3f04bf75e502c6b0 + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-provider@npm:^5.0.0": + version: 5.0.1 + resolution: "@metamask/eth-json-rpc-provider@npm:5.0.1" + dependencies: + "@metamask/json-rpc-engine": "npm:^10.1.1" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^11.8.1" + uuid: "npm:^8.3.2" + checksum: 10/1b874b04a0d8c53edf7036c8dbf4403101afba548cfd3bfc1fa969b893905a72e44c36a17b60ccb202f14a8e012b08031b4649c2ee77d604dc84dfbc6c88b812 + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-provider@npm:^6.0.0, @metamask/eth-json-rpc-provider@npm:^6.0.1": + version: 6.0.1 + resolution: "@metamask/eth-json-rpc-provider@npm:6.0.1" + dependencies: + "@metamask/json-rpc-engine": "npm:^10.2.4" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.9.0" + nanoid: "npm:^3.3.8" + checksum: 10/06078a9e43b02f35387a3ccfe09733c7eeac2a732dee1f1be53254fc05719e230776b8512b13702a178fb692088fc7da46f727c5064550d65f51ac59d44f9d83 + languageName: node + linkType: hard + "@metamask/eth-query@npm:^4.0.0": version: 4.0.0 resolution: "@metamask/eth-query@npm:4.0.0" @@ -1967,6 +2499,126 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-sig-util@npm:^8.2.0": + version: 8.2.0 + resolution: "@metamask/eth-sig-util@npm:8.2.0" + dependencies: + "@ethereumjs/rlp": "npm:^4.0.1" + "@ethereumjs/util": "npm:^8.1.0" + "@metamask/abi-utils": "npm:^3.0.0" + "@metamask/utils": "npm:^11.0.1" + "@scure/base": "npm:~1.1.3" + ethereum-cryptography: "npm:^2.1.2" + tweetnacl: "npm:^1.0.3" + checksum: 10/385df1ec541116e1bd725a1df1a519996bad167f99d1b2677126e398cdfda6fc3f03d2ff8f1ca523966bc0aae3ea92a9050953a45d5a7711f4128aacf9242bfc + languageName: node + linkType: hard + +"@metamask/eth-simple-keyring@npm:^12.0.2": + version: 12.0.2 + resolution: "@metamask/eth-simple-keyring@npm:12.0.2" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-sdk": "npm:^2.0.2" + "@metamask/utils": "npm:^11.11.0" + ethereum-cryptography: "npm:^2.2.1" + randombytes: "npm:^2.1.0" + checksum: 10/ac8a3a5871fb1b7503ae8714beaad07102ad473522f1c65cf09786a3f3498564763d96a51569ed712702f3a62dfaada4c9342def4d48e2c5a1f0985b5cd49725 + languageName: node + linkType: hard + +"@metamask/eth-snap-keyring@npm:^23.0.0": + version: 23.0.2 + resolution: "@metamask/eth-snap-keyring@npm:23.0.2" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/keyring-internal-api": "npm:^11.0.2" + "@metamask/keyring-internal-snap-client": "npm:^10.0.6" + "@metamask/keyring-sdk": "npm:^2.3.0" + "@metamask/keyring-snap-sdk": "npm:^9.2.1" + "@metamask/keyring-utils": "npm:^4.0.0" + "@metamask/messenger": "npm:^1.1.1" + "@metamask/snaps-controllers": "npm:^19.0.1" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.2.1" + "@metamask/superstruct": "npm:^3.4.1" + "@metamask/utils": "npm:^11.11.0" + "@types/uuid": "npm:^9.0.8" + async-mutex: "npm:^0.5.0" + uuid: "npm:^9.0.1" + peerDependencies: + "@metamask/keyring-api": ^23.0.0 + checksum: 10/a8763d5ec3c8863a6a623bd58b3328b06adba6435ea73b5f0b987f6ae2b8fa7a95a9464cc9d9a9ffec11d2183d36b1947251761328d15fb10038f368f6f7ffc5 + languageName: node + linkType: hard + +"@metamask/ethjs-contract@npm:^0.4.1": + version: 0.4.1 + resolution: "@metamask/ethjs-contract@npm:0.4.1" + dependencies: + "@metamask/ethjs-filter": "npm:^0.3.0" + "@metamask/ethjs-util": "npm:^0.3.0" + ethjs-abi: "npm:^0.2.0" + js-sha3: "npm:^0.9.2" + promise-to-callback: "npm:^1.0.0" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/2187f62336c2afd46b51bda87b96e1537dd7dfe0f621defc3cb5ccb68add482004674f3c11ced95c65b7ecbe845592541f1b268c97b0b42c939d8d5677523097 + languageName: node + linkType: hard + +"@metamask/ethjs-filter@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask/ethjs-filter@npm:0.3.0" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/1b7565045e638f60d39ae8d8ca020b6e3ac2e4e91f220f343e499b09a7adeb9e4273249e3c3f7366acc2e44bc0b7fe3dd3ac84c66e9a0853352bfb0e116c1d87 + languageName: node + linkType: hard + +"@metamask/ethjs-format@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask/ethjs-format@npm:0.3.0" + dependencies: + "@metamask/ethjs-util": "npm:^0.3.0" + "@metamask/number-to-bn": "npm:^1.7.1" + bn.js: "npm:^5.2.1" + ethjs-schema: "npm:0.2.1" + is-hex-prefixed: "npm:1.0.0" + strip-hex-prefix: "npm:1.0.0" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/4ae73716e0dd76f7b780ede79b3989d241ee8654b7aa9ec23678529a1916f77ab101552f7aabe56c8e26506a33f34181f40624a68a317e7cd3ae45fb0da2fa73 + languageName: node + linkType: hard + +"@metamask/ethjs-query@npm:^0.7.1": + version: 0.7.1 + resolution: "@metamask/ethjs-query@npm:0.7.1" + dependencies: + "@metamask/ethjs-format": "npm:^0.3.0" + "@metamask/ethjs-rpc": "npm:^0.4.0" + promise-to-callback: "npm:^1.0.0" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/a9d54677cf062267f7e781996edfadfc2a5a2f3589266b7f20386e8109b7cecdfdc47c6aba4bb68d0c4cb387cfdb463d6316d506c06138531ca1e81ab201e5dc + languageName: node + linkType: hard + +"@metamask/ethjs-rpc@npm:^0.4.0": + version: 0.4.0 + resolution: "@metamask/ethjs-rpc@npm:0.4.0" + dependencies: + promise-to-callback: "npm:^1.0.0" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/e126535ff7599ef584b0084dae846294d505b69141a143543d7580cdce4b297646e796fbd0cecc5a824dce8b5e3b179bb177ceff06fd50ad584e7fce00ab081f + languageName: node + linkType: hard + "@metamask/ethjs-unit@npm:^0.3.0": version: 0.3.0 resolution: "@metamask/ethjs-unit@npm:0.3.0" @@ -1979,6 +2631,40 @@ __metadata: languageName: node linkType: hard +"@metamask/ethjs-util@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask/ethjs-util@npm:0.3.0" + dependencies: + is-hex-prefixed: "npm:1.0.0" + strip-hex-prefix: "npm:1.0.0" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/aaecae091dd16362f048cbba27629d67d10530c704facf80f430db546c8ea32fa627cd9560bd3eca1e6ce651d2b7a7df9fdc3677e0ae2814954e3c950cee70da + languageName: node + linkType: hard + +"@metamask/gas-fee-controller@npm:^26.3.0": + version: 26.3.0 + resolution: "@metamask/gas-fee-controller@npm:26.3.0" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/ethjs-unit": "npm:^0.3.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/polling-controller": "npm:^16.0.8" + "@metamask/utils": "npm:^11.11.0" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + bn.js: "npm:^5.2.1" + uuid: "npm:^8.3.2" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/a09363e61f84287596eb31af6bc9bc77361a4f3aab4f7751aa85e7c94996bdfb088ecb2620404f1bd405129077043c9c2efd9d27f5a79a5dd5f7a7435fb02b82 + languageName: node + linkType: hard + "@metamask/internal-snaps-monorepo@workspace:.": version: 0.0.0-use.local resolution: "@metamask/internal-snaps-monorepo@workspace:." @@ -2029,7 +2715,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.4, @metamask/json-rpc-engine@npm:^10.5.0": +"@metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.4, @metamask/json-rpc-engine@npm:^10.5.0": version: 10.5.0 resolution: "@metamask/json-rpc-engine@npm:10.5.0" dependencies: @@ -2056,7 +2742,7 @@ __metadata: languageName: node linkType: hard -"@metamask/key-tree@npm:^10.1.1": +"@metamask/key-tree@npm:^10.0.2, @metamask/key-tree@npm:^10.1.1": version: 10.1.1 resolution: "@metamask/key-tree@npm:10.1.1" dependencies: @@ -2069,7 +2755,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^23.7.0": +"@metamask/keyring-api@npm:^23.1.0, @metamask/keyring-api@npm:^23.3.0, @metamask/keyring-api@npm:^23.5.0, @metamask/keyring-api@npm:^23.6.0, @metamask/keyring-api@npm:^23.7.0": version: 23.7.0 resolution: "@metamask/keyring-api@npm:23.7.0" dependencies: @@ -2081,6 +2767,87 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-controller@npm:^27.1.0": + version: 27.1.0 + resolution: "@metamask/keyring-controller@npm:27.1.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/browser-passworder": "npm:^6.0.0" + "@metamask/eth-hd-keyring": "npm:^14.1.1" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/eth-simple-keyring": "npm:^12.0.2" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/utils": "npm:^11.11.0" + async-mutex: "npm:^0.5.0" + ethereumjs-wallet: "npm:^1.0.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + ulid: "npm:^2.3.0" + checksum: 10/72c3cd205c88ac0becff32f1d22dae841d13a6847fc165a16782d1cd8daafb434b472ef867b94833d2f8bcb5f98a9663b630c8bed231332305ba1a395385c241 + languageName: node + linkType: hard + +"@metamask/keyring-internal-api@npm:^11.0.1, @metamask/keyring-internal-api@npm:^11.0.2": + version: 11.0.2 + resolution: "@metamask/keyring-internal-api@npm:11.0.2" + dependencies: + "@metamask/keyring-api": "npm:^23.6.0" + "@metamask/keyring-utils": "npm:^4.0.0" + "@metamask/superstruct": "npm:^3.4.1" + checksum: 10/7a5d817b93d3f0bd54860d7747833ace48e46d079e700d98f87c76a79b4da25acc1ba0aec3fdc4ff49c36fb3dc0a3378d27c7b970cdb31c88f8caa13c8199086 + languageName: node + linkType: hard + +"@metamask/keyring-internal-snap-client@npm:^10.0.5, @metamask/keyring-internal-snap-client@npm:^10.0.6": + version: 10.0.6 + resolution: "@metamask/keyring-internal-snap-client@npm:10.0.6" + dependencies: + "@metamask/keyring-api": "npm:^23.6.0" + "@metamask/keyring-internal-api": "npm:^11.0.2" + "@metamask/keyring-snap-client": "npm:^9.2.1" + "@metamask/keyring-utils": "npm:^4.0.0" + "@metamask/messenger": "npm:^1.1.1" + checksum: 10/b2325f9e9dfae8289574799b6b3cf4aa0a6c4d83a94baae48e1b47948977ad1444adba848ef06e7eec36e966b479485be82c6f9210b3dfbb3c2cdfc2a1e81ef4 + languageName: node + linkType: hard + +"@metamask/keyring-sdk@npm:^2.0.2, @metamask/keyring-sdk@npm:^2.2.0, @metamask/keyring-sdk@npm:^2.3.0": + version: 2.3.0 + resolution: "@metamask/keyring-sdk@npm:2.3.0" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/keyring-api": "npm:^23.6.0" + "@metamask/keyring-utils": "npm:^4.0.0" + "@metamask/scure-bip39": "npm:^2.1.1" + "@metamask/superstruct": "npm:^3.4.1" + "@metamask/utils": "npm:^11.11.0" + async-mutex: "npm:^0.5.0" + ethereum-cryptography: "npm:^2.2.1" + uuid: "npm:^9.0.1" + checksum: 10/38402f3834e0e364e1b17fe30ff64c1a050549239e204260081ddd0fc9228e9632d586819c551f91793d78207fb33f81e06ec3ac24537d511e8b5f1fabe57d21 + languageName: node + linkType: hard + +"@metamask/keyring-snap-client@npm:^9.2.0, @metamask/keyring-snap-client@npm:^9.2.1": + version: 9.2.1 + resolution: "@metamask/keyring-snap-client@npm:9.2.1" + dependencies: + "@metamask/keyring-api": "npm:^23.6.0" + "@metamask/keyring-utils": "npm:^4.0.0" + "@metamask/superstruct": "npm:^3.4.1" + "@types/uuid": "npm:^9.0.8" + uuid: "npm:^9.0.1" + webextension-polyfill: "npm:^0.12.0" + peerDependencies: + "@metamask/providers": ^19.0.0 + checksum: 10/bd4f006737598d44992e2a6283448a9cb4321fd0b5bbe59d37d8e28b1e31ad28cce997100e5af5a04c661faba1ee9f320df59ce86ef4c2a0fcbc653091b52af2 + languageName: node + linkType: hard + "@metamask/keyring-snap-sdk@npm:^9.2.1": version: 9.2.1 resolution: "@metamask/keyring-snap-sdk@npm:9.2.1" @@ -2097,6 +2864,18 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-utils@npm:^3.3.1": + version: 3.3.1 + resolution: "@metamask/keyring-utils@npm:3.3.1" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.11.0" + bitcoin-address-validation: "npm:^2.2.3" + checksum: 10/d0917b2f634d9eb2f563827739fca00c1675ee90674ec49b2b68afcb604aee843d6c5be5d79e9780fa22d29f71fc876f40b2d3d0ed4cab7f5948937ddd276691 + languageName: node + linkType: hard + "@metamask/keyring-utils@npm:^4.0.0": version: 4.0.0 resolution: "@metamask/keyring-utils@npm:4.0.0" @@ -2109,7 +2888,23 @@ __metadata: languageName: node linkType: hard -"@metamask/messenger@npm:^1.0.0, @metamask/messenger@npm:^1.1.1, @metamask/messenger@npm:^1.2.0": +"@metamask/message-manager@npm:^14.1.1": + version: 14.1.2 + resolution: "@metamask/message-manager@npm:14.1.2" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/utils": "npm:^11.9.0" + "@types/uuid": "npm:^8.3.0" + jsonschema: "npm:^1.4.1" + uuid: "npm:^8.3.2" + checksum: 10/82e3965069f0eb34e2141ca4dff452c118f54502d0b5d6b832af1906c6413ba1d923b13f1ed6af7f8a7b3087ef7f3b1a700bcffa56b10b06df53972ff2cf9a4f + languageName: node + linkType: hard + +"@metamask/messenger@npm:^1.0.0, @metamask/messenger@npm:^1.1.0, @metamask/messenger@npm:^1.1.1, @metamask/messenger@npm:^1.2.0": version: 1.2.0 resolution: "@metamask/messenger@npm:1.2.0" dependencies: @@ -2134,6 +2929,138 @@ __metadata: languageName: node linkType: hard +"@metamask/metamask-eth-abis@npm:^3.1.1": + version: 3.1.1 + resolution: "@metamask/metamask-eth-abis@npm:3.1.1" + checksum: 10/2d456882c5180eaf02d07f578ee8b8806d18639349d086a6b3b68c87e7ea294749efc332bfe7c1d5fb700933e71f31a46d3cd07b94c4e91c80faea5f9729bf0d + languageName: node + linkType: hard + +"@metamask/multichain-account-service@npm:^13.0.0": + version: 13.0.0 + resolution: "@metamask/multichain-account-service@npm:13.0.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/accounts-controller": "npm:^39.0.5" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/eth-snap-keyring": "npm:^23.0.0" + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/keyring-api": "npm:^23.5.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/keyring-snap-client": "npm:^9.2.0" + "@metamask/keyring-utils": "npm:^3.3.1" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/snap-account-service": "npm:^2.0.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.11.0" + async-mutex: "npm:^0.5.0" + lodash: "npm:^4.17.21" + peerDependencies: + "@metamask/account-api": ^1.0.4 + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/2ecb49a620420804ff74f1e6ce9a75303f2e41a5182b8570b6bef324c63e5be149ea1870815dbe8403a87c8fe17529322896fd9015e10766095a0d74c2aebafd + languageName: node + linkType: hard + +"@metamask/multichain-network-controller@npm:^3.2.1": + version: 3.2.1 + resolution: "@metamask/multichain-network-controller@npm:3.2.1" + dependencies: + "@metamask/accounts-controller": "npm:^39.0.4" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/keyring-api": "npm:^23.3.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.11.0" + "@solana/addresses": "npm:^2.0.0" + lodash: "npm:^4.17.21" + checksum: 10/60024909e413f4753ba7ef52295f4a5b102afc5a66cc5a5d61c15eca28a3ae452da2b081a47c4ef5c067b2faba7e69963f7da825f4fb59746cbb95ee6089f67a + languageName: node + linkType: hard + +"@metamask/network-controller@npm:^34.0.0": + version: 34.0.0 + resolution: "@metamask/network-controller@npm:34.0.0" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/connectivity-controller": "npm:^0.2.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/eth-block-tracker": "npm:^15.0.1" + "@metamask/eth-json-rpc-infura": "npm:^10.3.0" + "@metamask/eth-json-rpc-middleware": "npm:^23.1.3" + "@metamask/eth-json-rpc-provider": "npm:^6.0.1" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/json-rpc-engine": "npm:^10.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.2" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/swappable-obj-proxy": "npm:^2.3.0" + "@metamask/utils": "npm:^11.11.0" + fast-deep-equal: "npm:^3.1.3" + immer: "npm:^9.0.6" + loglevel: "npm:^1.8.1" + reselect: "npm:^5.1.1" + uri-js: "npm:^4.4.1" + uuid: "npm:^8.3.2" + checksum: 10/6afb874ca5702e1be616bf6ea92b35332d1969878794ce118138ca8f3733a1521a266d00bb658cf7a37e126545ac942ef5bc09bbd64159612b5424d848dbdc67 + languageName: node + linkType: hard + +"@metamask/network-enablement-controller@npm:^5.6.0": + version: 5.6.0 + resolution: "@metamask/network-enablement-controller@npm:5.6.0" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/keyring-api": "npm:^23.5.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/multichain-network-controller": "npm:^3.2.1" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/slip44": "npm:^4.3.0" + "@metamask/transaction-controller": "npm:^69.0.0" + "@metamask/utils": "npm:^11.11.0" + reselect: "npm:^5.1.1" + checksum: 10/c4fbedb43bc8331c24794c10a038809ec23b40a6042f589b3d72746a28846951c2e72c359ea6703b18840fc4fae6e5ffa7906979e58a1fb8f16f1fc735bd8514 + languageName: node + linkType: hard + +"@metamask/network-enablement-controller@npm:^6.0.0": + version: 6.0.0 + resolution: "@metamask/network-enablement-controller@npm:6.0.0" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/config-registry-controller": "npm:^1.0.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/keyring-api": "npm:^23.7.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/multichain-network-controller": "npm:^3.2.1" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/slip44": "npm:^4.3.0" + "@metamask/transaction-controller": "npm:^69.3.0" + "@metamask/utils": "npm:^11.11.0" + reselect: "npm:^5.1.1" + checksum: 10/51af8efed6887f8f99232ffde46ad82042224a8863606530301eedec6ff2b876c7b1373a981e5ae646a35de4f0623d98e497a6730cc485f68d6929be54923377 + languageName: node + linkType: hard + +"@metamask/nonce-tracker@npm:^6.0.0": + version: 6.1.0 + resolution: "@metamask/nonce-tracker@npm:6.1.0" + dependencies: + "@ethersproject/providers": "npm:^5.7.2" + async-mutex: "npm:^0.5.0" + checksum: 10/a20cf7084348ef7bdcd9e57e873ca34bf8e04ee43a2ab08634a3ad47fddb632d0e170c1dff03b4ca5bac34a24c35c542fcec509f3c23ce483834082f41b50e4a + languageName: node + linkType: hard + "@metamask/number-to-bn@npm:^1.7.1": version: 1.7.1 resolution: "@metamask/number-to-bn@npm:1.7.1" @@ -2154,7 +3081,7 @@ __metadata: languageName: node linkType: hard -"@metamask/permission-controller@npm:^12.2.0": +"@metamask/permission-controller@npm:^12.2.0, @metamask/permission-controller@npm:^12.3.0": version: 12.3.0 resolution: "@metamask/permission-controller@npm:12.3.0" dependencies: @@ -2192,6 +3119,38 @@ __metadata: languageName: node linkType: hard +"@metamask/phishing-controller@npm:^17.3.0": + version: 17.3.0 + resolution: "@metamask/phishing-controller@npm:17.3.0" + dependencies: + "@metamask/address-book-controller": "npm:^7.1.2" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/transaction-controller": "npm:^69.0.0" + "@noble/hashes": "npm:^1.8.0" + "@types/punycode": "npm:^2.1.0" + ethereum-cryptography: "npm:^2.1.2" + fastest-levenshtein: "npm:^1.0.16" + punycode: "npm:^2.1.1" + checksum: 10/5c9be5dfdd76c66197cf8eaa764da04d34e3f612aded6125c66d31a001430881ed00c59b5c04f30f05e9719a29332cfda0fbcf211b25b54c3ddaf0a812b8f70a + languageName: node + linkType: hard + +"@metamask/polling-controller@npm:^16.0.8": + version: 16.0.8 + resolution: "@metamask/polling-controller@npm:16.0.8" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/utils": "npm:^11.11.0" + "@types/uuid": "npm:^8.3.0" + fast-json-stable-stringify: "npm:^2.1.0" + uuid: "npm:^8.3.2" + checksum: 10/3066522432099d9d69475f73b4468da5ce18eb92340cffba2581ed8f0c8dfdb517ae704448fb395bc9a7659409c5a94cfb8a809b56bd35d7cd87de1b2d948dd8 + languageName: node + linkType: hard + "@metamask/post-message-stream@npm:^10.0.0": version: 10.0.0 resolution: "@metamask/post-message-stream@npm:10.0.0" @@ -2202,6 +3161,40 @@ __metadata: languageName: node linkType: hard +"@metamask/preferences-controller@npm:^23.1.0": + version: 23.1.0 + resolution: "@metamask/preferences-controller@npm:23.1.0" + dependencies: + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/messenger": "npm:^1.0.0" + checksum: 10/61fe1115546ea0c1e45143c2227d7907930ae881c3f14fe6bfb260d9d4e6fdfc84e3f46af69454daeef7dcbb7d02fdb204baf0c29bf05e18212479b6e96721a0 + languageName: node + linkType: hard + +"@metamask/profile-sync-controller@npm:^28.3.0": + version: 28.3.0 + resolution: "@metamask/profile-sync-controller@npm:28.3.0" + dependencies: + "@metamask/address-book-controller": "npm:^7.1.2" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/utils": "npm:^11.11.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/hashes": "npm:^1.8.0" + immer: "npm:^9.0.6" + loglevel: "npm:^1.8.1" + siwe: "npm:^2.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/a0fa6dad8a183bcd41907e7ab5d35fcb321bd1c7040125120deebc20b3d809a0e0f076feb2e80ef70b9ce1330138a8d1c8d8b5eea18086cd765ae4301a32e2ba + languageName: node + linkType: hard + "@metamask/providers@npm:^22.1.1": version: 22.1.1 resolution: "@metamask/providers@npm:22.1.1" @@ -2223,6 +3216,19 @@ __metadata: languageName: node linkType: hard +"@metamask/remote-feature-flag-controller@npm:4.2.2, @metamask/remote-feature-flag-controller@npm:^4.2.2": + version: 4.2.2 + resolution: "@metamask/remote-feature-flag-controller@npm:4.2.2" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.1.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/utils": "npm:^11.9.0" + uuid: "npm:^8.3.2" + checksum: 10/ed03ff1ba63c7a0f2b221c3514eb71a8be4200ec543b90676a44930dc731920ac2c92dcc2b13a32ed4e4e8e7e7b28a26db443af9f4bf30f5e2b5785580b286ab + languageName: node + linkType: hard + "@metamask/rpc-errors@npm:^7.0.2, @metamask/rpc-errors@npm:^7.0.3": version: 7.0.3 resolution: "@metamask/rpc-errors@npm:7.0.3" @@ -2268,13 +3274,32 @@ __metadata: languageName: node linkType: hard -"@metamask/slip44@npm:^4.2.0, @metamask/slip44@npm:^4.5.0": +"@metamask/slip44@npm:^4.2.0, @metamask/slip44@npm:^4.3.0, @metamask/slip44@npm:^4.5.0": version: 4.5.0 resolution: "@metamask/slip44@npm:4.5.0" checksum: 10/8450d4f5afa98e8f57c67a39f12aa413888c423505f22c05a3dc181be223f7742db017f54a99852acc3826abe74b22bf7247bccd78b810e509321c075e67516f languageName: node linkType: hard +"@metamask/snap-account-service@npm:^2.0.0": + version: 2.1.0 + resolution: "@metamask/snap-account-service@npm:2.1.0" + dependencies: + "@metamask/account-api": "npm:^1.1.1" + "@metamask/eth-snap-keyring": "npm:^23.0.0" + "@metamask/keyring-api": "npm:^23.7.0" + "@metamask/keyring-controller": "npm:^27.1.0" + "@metamask/keyring-internal-snap-client": "npm:^10.0.5" + "@metamask/keyring-snap-sdk": "npm:^9.2.1" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/utils": "npm:^11.11.0" + lodash: "npm:^4.17.21" + checksum: 10/00ad8d3c5daf0acc95c2c79ea914c5b802923441f2e0412ba01cef2226cbf2cb24bd0fb42ebb0f6078ccba5cddc39767ea3e401f24c429d3712857be401e3207 + languageName: node + linkType: hard + "@metamask/snaps-cli@npm:^8.4.1": version: 8.4.1 resolution: "@metamask/snaps-cli@npm:8.4.1" @@ -2328,6 +3353,49 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-controllers@npm:^19.0.0, @metamask/snaps-controllers@npm:^19.0.1": + version: 19.0.1 + resolution: "@metamask/snaps-controllers@npm:19.0.1" + dependencies: + "@metamask/approval-controller": "npm:^9.0.1" + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/json-rpc-engine": "npm:^10.2.4" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.8" + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/messenger": "npm:^1.1.0" + "@metamask/object-multiplex": "npm:^2.1.0" + "@metamask/permission-controller": "npm:^12.3.0" + "@metamask/post-message-stream": "npm:^10.0.0" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/snaps-registry": "npm:^4.0.0" + "@metamask/snaps-rpc-methods": "npm:^15.0.2" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.3" + "@metamask/storage-service": "npm:^1.0.1" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.10.0" + "@xstate/fsm": "npm:^2.0.0" + async-mutex: "npm:^0.5.0" + concat-stream: "npm:^2.0.0" + cron-parser: "npm:^4.5.0" + fast-deep-equal: "npm:^3.1.3" + get-npm-tarball-url: "npm:^2.0.3" + immer: "npm:^9.0.21" + luxon: "npm:^3.5.0" + nanoid: "npm:^3.3.10" + readable-stream: "npm:^3.6.2" + readable-web-to-node-stream: "npm:^3.0.2" + semver: "npm:^7.5.4" + tar-stream: "npm:^3.1.7" + peerDependencies: + "@metamask/snaps-execution-environments": ^11.0.2 + peerDependenciesMeta: + "@metamask/snaps-execution-environments": + optional: true + checksum: 10/e7cee816b1af1e2c4cbd25a81afff779427e6d738c1fe283a1ca9b23f67c62b5a983d6ed35e91acf0b15016e6825074557f3376d322a518c1acfc36728d36509 + languageName: node + linkType: hard + "@metamask/snaps-controllers@npm:^21.0.0": version: 21.0.0 resolution: "@metamask/snaps-controllers@npm:21.0.0" @@ -2439,6 +3507,23 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-rpc-methods@npm:^15.0.2": + version: 15.1.1 + resolution: "@metamask/snaps-rpc-methods@npm:15.1.1" + dependencies: + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/permission-controller": "npm:^12.3.0" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/snaps-sdk": "npm:^11.1.0" + "@metamask/snaps-utils": "npm:^12.2.0" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.11.0" + "@noble/hashes": "npm:^1.7.1" + async-mutex: "npm:^0.5.0" + checksum: 10/906eafa8a2d4944e50d73b8aa4d32a7070c33367ad3d58843e892e62b3bd8facb0a02e1d88a9245c18a281637435d97266212b7706ef67ee6a68db85a5991ea3 + languageName: node + linkType: hard + "@metamask/snaps-rpc-methods@npm:^17.0.0, @metamask/snaps-rpc-methods@npm:^17.1.0": version: 17.1.0 resolution: "@metamask/snaps-rpc-methods@npm:17.1.0" @@ -2507,7 +3592,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^12.0.0, @metamask/snaps-utils@npm:^12.1.0, @metamask/snaps-utils@npm:^12.2.1, @metamask/snaps-utils@npm:^12.3.0, @metamask/snaps-utils@npm:^12.4.0": +"@metamask/snaps-utils@npm:^12.0.0, @metamask/snaps-utils@npm:^12.1.0, @metamask/snaps-utils@npm:^12.1.2, @metamask/snaps-utils@npm:^12.1.3, @metamask/snaps-utils@npm:^12.2.0, @metamask/snaps-utils@npm:^12.2.1, @metamask/snaps-utils@npm:^12.3.0, @metamask/snaps-utils@npm:^12.4.0": version: 12.4.0 resolution: "@metamask/snaps-utils@npm:12.4.0" dependencies: @@ -2553,7 +3638,7 @@ __metadata: languageName: node linkType: hard -"@metamask/storage-service@npm:^1.0.2": +"@metamask/storage-service@npm:^1.0.1, @metamask/storage-service@npm:^1.0.2": version: 1.0.2 resolution: "@metamask/storage-service@npm:1.0.2" dependencies: @@ -2570,14 +3655,61 @@ __metadata: languageName: node linkType: hard +"@metamask/swappable-obj-proxy@npm:^2.3.0": + version: 2.3.0 + resolution: "@metamask/swappable-obj-proxy@npm:2.3.0" + checksum: 10/1255c599de9237f06df2390719d6dfcb1f168873df61bbaad5ce376efbc057e2030260b94855569313faeb412b7df9b062d209f4b0b163a3dc02f29d42139e1f + languageName: node + linkType: hard + +"@metamask/transaction-controller@npm:^69.0.0, @metamask/transaction-controller@npm:^69.2.1, @metamask/transaction-controller@npm:^69.3.0": + version: 69.3.0 + resolution: "@metamask/transaction-controller@npm:69.3.0" + dependencies: + "@ethereumjs/common": "npm:^4.4.0" + "@ethereumjs/tx": "npm:^5.4.0" + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/wallet": "npm:^5.7.0" + "@metamask/accounts-controller": "npm:^39.0.5" + "@metamask/approval-controller": "npm:^9.0.2" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/core-backend": "npm:^8.0.0" + "@metamask/gas-fee-controller": "npm:^26.3.0" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/network-controller": "npm:^34.0.0" + "@metamask/nonce-tracker": "npm:^6.0.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.2" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.11.0" + async-mutex: "npm:^0.5.0" + bignumber.js: "npm:^9.1.2" + bn.js: "npm:^5.2.1" + eth-method-registry: "npm:^4.0.0" + fast-json-patch: "npm:^3.1.1" + lodash: "npm:^4.17.21" + uuid: "npm:^8.3.2" + peerDependencies: + "@babel/runtime": ^7.0.0 + "@metamask/eth-block-tracker": ">=9" + checksum: 10/93923f7e990248a7366f1ad715e8a4eda50bbd0423193e3875109d6733b8ab22d246e4167b740cb3e296bfa0eb10c69b3b7601cdb17f60e016a9837102c8f710 + languageName: node + linkType: hard + "@metamask/tron-wallet-snap@workspace:packages/tron-wallet-snap": version: 0.0.0-use.local resolution: "@metamask/tron-wallet-snap@workspace:packages/tron-wallet-snap" dependencies: + "@metamask/assets-controller": "npm:11.2.0" "@metamask/auto-changelog": "npm:^6.1.1" "@metamask/key-tree": "npm:^10.1.1" "@metamask/keyring-api": "npm:^23.7.0" "@metamask/keyring-snap-sdk": "npm:^9.2.1" + "@metamask/messenger": "npm:^2.0.0" + "@metamask/remote-feature-flag-controller": "npm:4.2.2" "@metamask/snaps-cli": "npm:^8.4.1" "@metamask/snaps-jest": "npm:^10.2.0" "@metamask/snaps-sdk": "npm:^11.2.0" @@ -2644,6 +3776,13 @@ __metadata: languageName: node linkType: hard +"@noble/ciphers@npm:^1.3.0": + version: 1.3.0 + resolution: "@noble/ciphers@npm:1.3.0" + checksum: 10/051660051e3e9e2ca5fb9dece2885532b56b7e62946f89afa7284a0fb8bc02e2bd1c06554dba68162ff42d295b54026456084198610f63c296873b2f1cd7a586 + languageName: node + linkType: hard + "@noble/curves@npm:1.2.0": version: 1.2.0 resolution: "@noble/curves@npm:1.2.0" @@ -2685,7 +3824,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.7.1": +"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.7.1, @noble/hashes@npm:^1.8.0": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" checksum: 10/474b7f56bc6fb2d5b3a42132561e221b0ea4f91e590f4655312ca13667840896b34195e2b53b7f097ec080a1fdd3b58d902c2a8d0fbdf51d2e238b53808a177e @@ -3233,30 +4372,116 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^3.0.0, @sinonjs/commons@npm:^3.0.1": - version: 3.0.1 - resolution: "@sinonjs/commons@npm:3.0.1" +"@sinonjs/commons@npm:^3.0.0, @sinonjs/commons@npm:^3.0.1": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" + dependencies: + type-detect: "npm:4.0.8" + checksum: 10/a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^10.0.2": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": "npm:^3.0.0" + checksum: 10/78155c7bd866a85df85e22028e046b8d46cf3e840f72260954f5e3ed5bd97d66c595524305a6841ffb3f681a08f6e5cef572a2cce5442a8a232dc29fb409b83e + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^13.0.0": + version: 13.0.5 + resolution: "@sinonjs/fake-timers@npm:13.0.5" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f + languageName: node + linkType: hard + +"@solana/addresses@npm:^2.0.0": + version: 2.3.0 + resolution: "@solana/addresses@npm:2.3.0" + dependencies: + "@solana/assertions": "npm:2.3.0" + "@solana/codecs-core": "npm:2.3.0" + "@solana/codecs-strings": "npm:2.3.0" + "@solana/errors": "npm:2.3.0" + "@solana/nominal-types": "npm:2.3.0" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/11ee6774f938dd2fe5174375b02c4e52063173ce80a895dd35beb84635dcf3e2ba2e3fe7c74a614b2322da10906abceab758c91a13415a9cda53faf91a8ea722 + languageName: node + linkType: hard + +"@solana/assertions@npm:2.3.0": + version: 2.3.0 + resolution: "@solana/assertions@npm:2.3.0" + dependencies: + "@solana/errors": "npm:2.3.0" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/3b430f01d5991569059fea3e6679a4548a624ff5da6db0c2eb2eba757a9ff774c9d90941a0a2335148599aaef8e8fc004a9c5996eb3627b25dca108b05591d02 + languageName: node + linkType: hard + +"@solana/codecs-core@npm:2.3.0": + version: 2.3.0 + resolution: "@solana/codecs-core@npm:2.3.0" + dependencies: + "@solana/errors": "npm:2.3.0" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/d9bba1eaa3ee38fef04e1cbfa43defeea16729a1cf1628a71cb72340558a3f2296279899680e1dda4b1756ab2b280b5f1502330c21c35e167e554f3d0c9d193d + languageName: node + linkType: hard + +"@solana/codecs-numbers@npm:2.3.0": + version: 2.3.0 + resolution: "@solana/codecs-numbers@npm:2.3.0" + dependencies: + "@solana/codecs-core": "npm:2.3.0" + "@solana/errors": "npm:2.3.0" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/e661338b5eb04268a104ff2189b5d001bd2f99e1a3726deaa7157d5acbc3b24740bc25ca03b2028c52ad21fd71d5d5aa64957411c895a9dc1c132aa3bc97b336 + languageName: node + linkType: hard + +"@solana/codecs-strings@npm:2.3.0": + version: 2.3.0 + resolution: "@solana/codecs-strings@npm:2.3.0" dependencies: - type-detect: "npm:4.0.8" - checksum: 10/a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117 + "@solana/codecs-core": "npm:2.3.0" + "@solana/codecs-numbers": "npm:2.3.0" + "@solana/errors": "npm:2.3.0" + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ">=5.3.3" + checksum: 10/a553a891ce149a87f37ff338587980e81f9752e13039cb0d02f0fc6f21ce27af8548ad10f4c9e63f9ef74786ea0e498c91a2f5e3e996a930f581f2977bb1c215 languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^10.0.2": - version: 10.3.0 - resolution: "@sinonjs/fake-timers@npm:10.3.0" +"@solana/errors@npm:2.3.0": + version: 2.3.0 + resolution: "@solana/errors@npm:2.3.0" dependencies: - "@sinonjs/commons": "npm:^3.0.0" - checksum: 10/78155c7bd866a85df85e22028e046b8d46cf3e840f72260954f5e3ed5bd97d66c595524305a6841ffb3f681a08f6e5cef572a2cce5442a8a232dc29fb409b83e + chalk: "npm:^5.4.1" + commander: "npm:^14.0.0" + peerDependencies: + typescript: ">=5.3.3" + bin: + errors: bin/cli.mjs + checksum: 10/0e8a329790b7d38b4bfe1fa6ec2ac60be20562a610d992031395fe9886da28b578a9d0aebb318f5357ae0d4cbc8f3d323c12b9520da2cf6adc9038f96afc3fe1 languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^13.0.0": - version: 13.0.5 - resolution: "@sinonjs/fake-timers@npm:13.0.5" - dependencies: - "@sinonjs/commons": "npm:^3.0.1" - checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f +"@solana/nominal-types@npm:2.3.0": + version: 2.3.0 + resolution: "@solana/nominal-types@npm:2.3.0" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/0594893661f4ff2f8587689cd4b61ee15c38c455fe5cbaa7ae7e416f3a483fac97cc3f5a5b3d0a7526bfb89d7da91bc2c72e7b1790bbe59b986579ef2f76689b languageName: node linkType: hard @@ -3272,6 +4497,51 @@ __metadata: languageName: node linkType: hard +"@spruceid/siwe-parser@npm:^2.1.2": + version: 2.1.2 + resolution: "@spruceid/siwe-parser@npm:2.1.2" + dependencies: + "@noble/hashes": "npm:^1.1.2" + apg-js: "npm:^4.3.0" + uri-js: "npm:^4.4.1" + valid-url: "npm:^1.0.9" + checksum: 10/48459fe3b4d4b3091375ee87af700864c9023d4a1271d34850c6d27475e5d93a45d1efe8a71da367ad838b6921ced60c387d54737edd0a7a0d8e4e0a3cc2b8b7 + languageName: node + linkType: hard + +"@stablelib/binary@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/binary@npm:1.0.1" + dependencies: + "@stablelib/int": "npm:^1.0.1" + checksum: 10/c5ed769e2b5d607a5cdb72d325fcf98db437627862fade839daad934bd9ccf02a6f6e34f9de8cb3b18d72fce2ba6cc019a5d22398187d7d69d2607165f27f8bf + languageName: node + linkType: hard + +"@stablelib/int@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/int@npm:1.0.1" + checksum: 10/65bfbf50a382eea70c68e05366bf379cfceff8fbc076f1c267ef2f2411d7aed64fd140c415cb6c29f19a3910d3b8b7805d4b32ad5721a5007a8e744a808c7ae3 + languageName: node + linkType: hard + +"@stablelib/random@npm:^1.0.1": + version: 1.0.2 + resolution: "@stablelib/random@npm:1.0.2" + dependencies: + "@stablelib/binary": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/f5ace0a588dc4c21f01cb85837892d4c872e994ae77a58a8eb7dd61aa0b26fb1e9b46b0445e71af57d963ef7d9f5965c64258fc0d04df7b2947bc48f2d3560c5 + languageName: node + linkType: hard + +"@stablelib/wipe@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/wipe@npm:1.0.1" + checksum: 10/287802eb146810a46ba72af70b82022caf83a8aeebde23605f5ee0decf64fe2b97a60c856e43b6617b5801287c30cfa863cfb0469e7fcde6f02d143cf0c6cbf4 + languageName: node + linkType: hard + "@swc/core-darwin-arm64@npm:1.11.31": version: 1.11.31 resolution: "@swc/core-darwin-arm64@npm:1.11.31" @@ -3404,6 +4674,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:^5.62.16": + version: 5.101.4 + resolution: "@tanstack/query-core@npm:5.101.4" + checksum: 10/df86031035ac9a5cf54efece85718b90e93fca62068a44a176da232b6dd49b27168840f2da18c64a0eb25d171bc3fb1aab417db4643025cd194ff887833bd73f + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.10.3": version: 0.10.3 resolution: "@tybys/wasm-util@npm:0.10.3" @@ -3454,7 +4731,7 @@ __metadata: languageName: node linkType: hard -"@types/bn.js@npm:^5.1.5": +"@types/bn.js@npm:^5.1.0, @types/bn.js@npm:^5.1.5": version: 5.2.0 resolution: "@types/bn.js@npm:5.2.0" dependencies: @@ -3588,6 +4865,15 @@ __metadata: languageName: node linkType: hard +"@types/pbkdf2@npm:^3.0.0": + version: 3.1.2 + resolution: "@types/pbkdf2@npm:3.1.2" + dependencies: + "@types/node": "npm:*" + checksum: 10/bebe1e596cbbe5f7d2726a58859e61986c5a42459048e29cb7f2d4d764be6bbb0844572fd5d70ca8955a8a17e8b4ed80984fc4903e165d9efb8807a3fbb051aa + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.15 resolution: "@types/prop-types@npm:15.7.15" @@ -3595,6 +4881,13 @@ __metadata: languageName: node linkType: hard +"@types/punycode@npm:^2.1.0": + version: 2.1.4 + resolution: "@types/punycode@npm:2.1.4" + checksum: 10/16637bdd8e4f830243072668125f83b93b728085a05140ccc3e7801528d78c62cce5426b07d5cdc75e4f797e1644807c762777f651d1cd071ad0128835cdce5e + languageName: node + linkType: hard + "@types/react-dom@npm:18.2.4": version: 18.2.4 resolution: "@types/react-dom@npm:18.2.4" @@ -3622,6 +4915,15 @@ __metadata: languageName: node linkType: hard +"@types/secp256k1@npm:^4.0.1": + version: 4.0.7 + resolution: "@types/secp256k1@npm:4.0.7" + dependencies: + "@types/node": "npm:*" + checksum: 10/bcf7adbd953d2cb829ae98a3cd2f8eeb1e2316f4f6de720ee3863da90bd98899c08fa8c42748e8701cad16ede0898df69a6e7f22660d1fb3cb276d5c771069db + languageName: node + linkType: hard + "@types/semver@npm:^7, @types/semver@npm:^7.3.6": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" @@ -3636,6 +4938,20 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^8.3.0": + version: 8.3.4 + resolution: "@types/uuid@npm:8.3.4" + checksum: 10/6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f + languageName: node + linkType: hard + +"@types/uuid@npm:^9.0.8": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: 10/b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -4271,6 +5587,13 @@ __metadata: languageName: node linkType: hard +"aes-js@npm:3.0.0": + version: 3.0.0 + resolution: "aes-js@npm:3.0.0" + checksum: 10/1b3772e5ba74abdccb6c6b99bf7f50b49057b38c0db1612b46c7024414f16e65ba7f1643b2d6e38490b1870bdf3ba1b87b35e2c831fd3fdaeff015f08aad19d1 + languageName: node + linkType: hard + "aes-js@npm:4.0.0-beta.5": version: 4.0.0-beta.5 resolution: "aes-js@npm:4.0.0-beta.5" @@ -4278,6 +5601,13 @@ __metadata: languageName: node linkType: hard +"aes-js@npm:^3.1.2": + version: 3.1.2 + resolution: "aes-js@npm:3.1.2" + checksum: 10/b65916767034a51375a3ac5aad62af452d89a386c1ae7b607bb9145d0bb8b8823bf2f3eba85bdfa52d61c65d5aed90ba90f677b8c826bfa1a8b7ae2fa3b54d91 + languageName: node + linkType: hard + "agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": version: 7.1.1 resolution: "agent-base@npm:7.1.1" @@ -4418,7 +5748,7 @@ __metadata: languageName: node linkType: hard -"apg-js@npm:^4.1.1": +"apg-js@npm:^4.1.1, apg-js@npm:^4.3.0": version: 4.4.0 resolution: "apg-js@npm:4.4.0" checksum: 10/425f19096026742f5f156f26542b68f55602aa60f0c4ae2d72a0a888cf15fe9622223191202262dd8979d76a6125de9d8fd164d56c95fb113f49099f405eb08c @@ -4789,7 +6119,7 @@ __metadata: languageName: node linkType: hard -"bech32@npm:^1.1.3": +"bech32@npm:1.1.4, bech32@npm:^1.1.3": version: 1.1.4 resolution: "bech32@npm:1.1.4" checksum: 10/63ff37c0ce43be914c685ce89700bba1589c319af0dac1ea04f51b33d0e5ecfd40d14c24f527350b94f0a4e236385373bb9122ec276410f354ddcdbf29ca13f4 @@ -4926,6 +6256,20 @@ __metadata: languageName: node linkType: hard +"blakejs@npm:^1.1.0": + version: 1.2.1 + resolution: "blakejs@npm:1.2.1" + checksum: 10/0638b1bd058b21892633929c43005aa6a4cc4b2ac5b338a146c3c076622f1b360795bd7a4d1f077c9b01863ed2df0c1504a81c5b520d164179120434847e6cd7 + languageName: node + linkType: hard + +"bn.js@npm:4.11.6": + version: 4.11.6 + resolution: "bn.js@npm:4.11.6" + checksum: 10/22741b015c9fff60fce32fc9988331b298eb9b6db5bfb801babb23b846eaaf894e440e0d067b2b3ae4e46aab754e90972f8f333b31bf94a686bbcb054bfa7b14 + languageName: node + linkType: hard + "bn.js@npm:5.2.1": version: 5.2.1 resolution: "bn.js@npm:5.2.1" @@ -4940,7 +6284,7 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:^5.2.1, bn.js@npm:^5.2.3": +"bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1, bn.js@npm:^5.2.3": version: 5.2.5 resolution: "bn.js@npm:5.2.5" checksum: 10/10b1ac91e76fd828c1187cf65a9f1a0374e7afb9acd167e19c2d7c4108f6216841007c1e3aef145fa95cc7b3d07f705236a707184af1537667362b3a842d01ac @@ -5335,7 +6679,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:5.6.2": +"chalk@npm:5.6.2, chalk@npm:^5.4.1": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 @@ -5555,6 +6899,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^14.0.0": + version: 14.0.3 + resolution: "commander@npm:14.0.3" + checksum: 10/dfa9ebe2a433d277de5cb0252d23b10a543d245d892db858d23b516336a835c50fd4f52bee4cd13c705cc8acb6f03dc632c73dd806f7d06d3353eb09953dd17a + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -6779,7 +8130,19 @@ __metadata: languageName: node linkType: hard -"ethereum-cryptography@npm:2.2.1, ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.2.1": +"eth-method-registry@npm:^4.0.0": + version: 4.0.0 + resolution: "eth-method-registry@npm:4.0.0" + dependencies: + "@metamask/ethjs-contract": "npm:^0.4.1" + "@metamask/ethjs-query": "npm:^0.7.1" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/959e833327960f40fab8693997e071def75103a694e931ab11313e642b6958d4f2878000d91ddfd6ba3c158bdc3f381c63eae0dd26738790e2aca5004ceb0387 + languageName: node + linkType: hard + +"ethereum-cryptography@npm:2.2.1, ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.1.2, ethereum-cryptography@npm:^2.2.1": version: 2.2.1 resolution: "ethereum-cryptography@npm:2.2.1" dependencies: @@ -6791,6 +8154,58 @@ __metadata: languageName: node linkType: hard +"ethereum-cryptography@npm:^0.1.3": + version: 0.1.3 + resolution: "ethereum-cryptography@npm:0.1.3" + dependencies: + "@types/pbkdf2": "npm:^3.0.0" + "@types/secp256k1": "npm:^4.0.1" + blakejs: "npm:^1.1.0" + browserify-aes: "npm:^1.2.0" + bs58check: "npm:^2.1.2" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + hash.js: "npm:^1.1.7" + keccak: "npm:^3.0.0" + pbkdf2: "npm:^3.0.17" + randombytes: "npm:^2.1.0" + safe-buffer: "npm:^5.1.2" + scrypt-js: "npm:^3.0.0" + secp256k1: "npm:^4.0.1" + setimmediate: "npm:^1.0.5" + checksum: 10/975e476782746acd97d5b37366801ae622a52fb31e5d83f600804be230a61ef7b9d289dcecd9c308fb441967caf3a6e3768dd7c8add6441fcc60c398175d5a96 + languageName: node + linkType: hard + +"ethereumjs-util@npm:^7.1.2": + version: 7.1.5 + resolution: "ethereumjs-util@npm:7.1.5" + dependencies: + "@types/bn.js": "npm:^5.1.0" + bn.js: "npm:^5.1.2" + create-hash: "npm:^1.1.2" + ethereum-cryptography: "npm:^0.1.3" + rlp: "npm:^2.2.4" + checksum: 10/f28fc1ebb8f35bf9e418f76f51be737d94d603b912c3e014c4e87cd45ccd1b10bdfef764c8f152574b57e9faa260a18773cbc110f9e0a754d6b3730699e54dc9 + languageName: node + linkType: hard + +"ethereumjs-wallet@npm:^1.0.1": + version: 1.0.2 + resolution: "ethereumjs-wallet@npm:1.0.2" + dependencies: + aes-js: "npm:^3.1.2" + bs58check: "npm:^2.1.2" + ethereum-cryptography: "npm:^0.1.3" + ethereumjs-util: "npm:^7.1.2" + randombytes: "npm:^2.1.0" + scrypt-js: "npm:^3.0.1" + utf8: "npm:^3.0.0" + uuid: "npm:^8.3.2" + checksum: 10/0a9ad0ac0930627c15e6fa6115dde8d1dd81160e57fbdf81ef0bcd1e0edd750fe9e8b00992651e694cabefee29eef2ad651dce8b24ae5253861927d0e19b6c90 + languageName: node + linkType: hard + "ethers@npm:6.13.5": version: 6.13.5 resolution: "ethers@npm:6.13.5" @@ -6821,6 +8236,24 @@ __metadata: languageName: node linkType: hard +"ethjs-abi@npm:^0.2.0": + version: 0.2.1 + resolution: "ethjs-abi@npm:0.2.1" + dependencies: + bn.js: "npm:4.11.6" + js-sha3: "npm:0.5.5" + number-to-bn: "npm:1.7.0" + checksum: 10/733b2b95ae102f0813ad70848d42424b4b6204598892bee0e0fb435aafc7d356e479e489ee2d6b81fb2bd6e5ae6a9cdc0479bc35244b2efbe1a00a8da6735fbb + languageName: node + linkType: hard + +"ethjs-schema@npm:0.2.1": + version: 0.2.1 + resolution: "ethjs-schema@npm:0.2.1" + checksum: 10/9e2806518e208650a035aa2bc1e508a57c31ff629b46b63de4c7101e760f5e6987e6f8b70522eb44cf4a97d2c4c6348d1adaeea56915e75e4deadd1e176d5aa9 + languageName: node + linkType: hard + "event-target-shim@npm:^5.0.0": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" @@ -7080,6 +8513,13 @@ __metadata: languageName: node linkType: hard +"fast-json-patch@npm:^3.1.1": + version: 3.1.1 + resolution: "fast-json-patch@npm:3.1.1" + checksum: 10/3e56304e1c95ad1862a50e5b3f557a74c65c0ff2ba5b15caab983b43e70e86ddbc5bc887e9f7064f0aacfd0f0435a29ab2f000fe463379e72b906486345e6671 + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -7140,6 +8580,13 @@ __metadata: languageName: node linkType: hard +"fastest-levenshtein@npm:^1.0.16": + version: 1.0.16 + resolution: "fastest-levenshtein@npm:1.0.16" + checksum: 10/ee85d33b5cef592033f70e1c13ae8624055950b4eb832435099cd56aa313d7f251b873bedbc06a517adfaff7b31756d139535991e2406967438e03a1bf1b008e + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" @@ -7739,7 +9186,7 @@ __metadata: languageName: node linkType: hard -"hash.js@npm:1.1.7, hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": +"hash.js@npm:1.1.7, hash.js@npm:^1.0.0, hash.js@npm:^1.0.3, hash.js@npm:^1.1.7": version: 1.1.7 resolution: "hash.js@npm:1.1.7" dependencies: @@ -8072,6 +9519,13 @@ __metadata: languageName: node linkType: hard +"is-fn@npm:^1.0.0": + version: 1.0.0 + resolution: "is-fn@npm:1.0.0" + checksum: 10/eeea1e536716f93a92dc1a8550b2c0909fe74bb5144d0fb6d65e0d31eb9c06c30559f69d83a9351d2288cc7293b43bc074e0fab5fae19e312ff38aa0c7672827 + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -9092,6 +10546,13 @@ __metadata: languageName: node linkType: hard +"js-sha3@npm:0.5.5": + version: 0.5.5 + resolution: "js-sha3@npm:0.5.5" + checksum: 10/9ce8bfabdba2cfb94b911125fc278e2f46cc01b6590c98833d9361199e2c2b4bca0427d04da6aa083f05c2c3982029200964a3d6e417b0c126c80f2e32c2d5eb + languageName: node + linkType: hard + "js-sha3@npm:0.8.0": version: 0.8.0 resolution: "js-sha3@npm:0.8.0" @@ -9106,6 +10567,13 @@ __metadata: languageName: node linkType: hard +"js-sha3@npm:^0.9.2": + version: 0.9.3 + resolution: "js-sha3@npm:0.9.3" + checksum: 10/8daacb93b18609a0dc081f2f6199b80a96df36f9975b4b9c7476ae92822e07100b9e1969fc76f4b58e703cd6175f0de7656a99cbb2335cfb554c66f988fbead5 + languageName: node + linkType: hard + "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -9180,7 +10648,7 @@ __metadata: languageName: node linkType: hard -"json-rpc-random-id@npm:^1.0.0": +"json-rpc-random-id@npm:^1.0.0, json-rpc-random-id@npm:^1.0.1": version: 1.0.1 resolution: "json-rpc-random-id@npm:1.0.1" checksum: 10/fcd2e884193a129ace4002bd65a86e9cdb206733b4693baea77bd8b372cf8de3043fbea27716a2c9a716581a908ca8d978d9dfec4847eb2cf77edb4cf4b2252c @@ -9237,6 +10705,25 @@ __metadata: languageName: node linkType: hard +"jsonschema@npm:^1.4.1": + version: 1.5.0 + resolution: "jsonschema@npm:1.5.0" + checksum: 10/46bf49b388ba922073bcb3c8d5e90af9d29fc8303dc866fd440182c88d6b4fd2807679fd39cdefb4113156d104ea47da9c0ff4bbcb0032c9fa29461cb1a92182 + languageName: node + linkType: hard + +"keccak@npm:^3.0.0": + version: 3.0.4 + resolution: "keccak@npm:3.0.4" + dependencies: + node-addon-api: "npm:^2.0.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.2.0" + readable-stream: "npm:^3.6.0" + checksum: 10/45478bb0a57e44d0108646499b8360914b0fbc8b0e088f1076659cb34faaa9eb829c40f6dd9dadb3460bb86cc33153c41fed37fe5ce09465a60e71e78c23fa55 + languageName: node + linkType: hard + "keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -9347,6 +10834,13 @@ __metadata: languageName: node linkType: hard +"loglevel@npm:^1.8.1": + version: 1.9.2 + resolution: "loglevel@npm:1.9.2" + checksum: 10/6153d8db308323f7ee20130bc40309e7a976c30a10379d8666b596d9c6441965c3e074c8d7ee3347fe5cfc059c0375b6f3e8a10b93d5b813cc5547f5aa412a29 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -9764,6 +11258,13 @@ __metadata: languageName: node linkType: hard +"multiformats@npm:^9.9.0": + version: 9.9.0 + resolution: "multiformats@npm:9.9.0" + checksum: 10/ad55c7d480d22f4258a68fd88aa2aab744fe0cb1e68d732fc886f67d858b37e3aa6c2cec12b2960ead7730d43be690931485238569952d8a3d7f90fdc726c652 + languageName: node + linkType: hard + "multimatch@npm:^5.0.0": version: 5.0.0 resolution: "multimatch@npm:5.0.0" @@ -9864,6 +11365,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^2.0.0": + version: 2.0.2 + resolution: "node-addon-api@npm:2.0.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10/e4ce4daac5b2fefa6b94491b86979a9c12d9cceba571d2c6df1eb5859f9da68e5dc198f128798e1785a88aafee6e11f4992dcccd4bf86bec90973927d158bd60 + languageName: node + linkType: hard + "node-addon-api@npm:^5.0.0": version: 5.1.0 resolution: "node-addon-api@npm:5.1.0" @@ -10019,6 +11529,16 @@ __metadata: languageName: node linkType: hard +"number-to-bn@npm:1.7.0": + version: 1.7.0 + resolution: "number-to-bn@npm:1.7.0" + dependencies: + bn.js: "npm:4.11.6" + strip-hex-prefix: "npm:1.0.0" + checksum: 10/702e8f00b6b90abd23f711056005179c3bd5ce3b063c47d468250f63ab3b9b4b82e27bff3b4642a9e71e06c717d5ed359873501746df0a64c3db1fa6d704e704 + languageName: node + linkType: hard + "object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" @@ -10425,7 +11945,7 @@ __metadata: languageName: node linkType: hard -"pbkdf2@npm:^3.1.2, pbkdf2@npm:^3.1.5": +"pbkdf2@npm:^3.0.17, pbkdf2@npm:^3.1.2, pbkdf2@npm:^3.1.5": version: 3.1.6 resolution: "pbkdf2@npm:3.1.6" dependencies: @@ -10460,6 +11980,13 @@ __metadata: languageName: node linkType: hard +"pify@npm:^5.0.0": + version: 5.0.0 + resolution: "pify@npm:5.0.0" + checksum: 10/443e3e198ad6bfa8c0c533764cf75c9d5bc976387a163792fb553ffe6ce923887cf14eebf5aea9b7caa8eab930da8c33612990ae85bd8c2bc18bedb9eae94ecb + languageName: node + linkType: hard + "pirates@npm:^4.0.4, pirates@npm:^4.0.7": version: 4.0.7 resolution: "pirates@npm:4.0.7" @@ -10616,6 +12143,16 @@ __metadata: languageName: node linkType: hard +"promise-to-callback@npm:^1.0.0": + version: 1.0.0 + resolution: "promise-to-callback@npm:1.0.0" + dependencies: + is-fn: "npm:^1.0.0" + set-immediate-shim: "npm:^1.0.1" + checksum: 10/d756ad74f7a202a6725c4281813c1052896327e4036c553adb8e3bbb91d0b1ff501965e7b00997827c016609caefbf24a5efc117dccca6000b8cbbc477b31cbf + languageName: node + linkType: hard + "propagate@npm:^2.0.0": version: 2.0.1 resolution: "propagate@npm:2.0.1" @@ -10668,7 +12205,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0, punycode@npm:^2.3.0": +"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 @@ -10903,6 +12440,13 @@ __metadata: languageName: node linkType: hard +"reselect@npm:^5.1.1": + version: 5.2.0 + resolution: "reselect@npm:5.2.0" + checksum: 10/e53d37a35f84132682b0a819d942ff7debea90fe126f4bb5eef0f21b1f48f45dec89d27990f2ef5865f5e05d64bb88fb41e4341eb276674aee6f1f7f7362c3e8 + languageName: node + linkType: hard + "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -11059,6 +12603,17 @@ __metadata: languageName: node linkType: hard +"rlp@npm:^2.2.4": + version: 2.2.7 + resolution: "rlp@npm:2.2.7" + dependencies: + bn.js: "npm:^5.2.0" + bin: + rlp: bin/rlp + checksum: 10/cf1919a2dc99f336191b3363b76299db567c192b7ee3c6f5c722728c34f65577883c9c88eeb7a1bfcbc26693c8a4f1fb0662e79ee86f0c98dd258d6987303498 + languageName: node + linkType: hard + "router@npm:^2.2.0": version: 2.2.0 resolution: "router@npm:2.2.0" @@ -11129,6 +12684,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.4.3": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -11159,6 +12721,13 @@ __metadata: languageName: node linkType: hard +"scrypt-js@npm:3.0.1, scrypt-js@npm:^3.0.0, scrypt-js@npm:^3.0.1": + version: 3.0.1 + resolution: "scrypt-js@npm:3.0.1" + checksum: 10/2f8aa72b7f76a6f9c446bbec5670f80d47497bccce98474203d89b5667717223eeb04a50492ae685ed7adc5a060fc2d8f9fd988f8f7ebdaf3341967f3aeff116 + languageName: node + linkType: hard + "secp256k1@npm:^3.0.1": version: 3.8.1 resolution: "secp256k1@npm:3.8.1" @@ -11176,6 +12745,18 @@ __metadata: languageName: node linkType: hard +"secp256k1@npm:^4.0.1": + version: 4.0.4 + resolution: "secp256k1@npm:4.0.4" + dependencies: + elliptic: "npm:^6.5.7" + node-addon-api: "npm:^5.0.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.2.0" + checksum: 10/45000f348c853df7c1e2b67c48efb062ae78c0620ab1a5cfb02fa20d3aad39c641f4e7a18b3de3b54a7c0cc1e0addeb8ecd9d88bc332e92df17a92b60c36122a + languageName: node + linkType: hard + "secp256k1@npm:^5.0.1": version: 5.0.1 resolution: "secp256k1@npm:5.0.1" @@ -11311,7 +12892,14 @@ __metadata: languageName: node linkType: hard -"setimmediate@npm:^1.0.4": +"set-immediate-shim@npm:^1.0.1": + version: 1.0.1 + resolution: "set-immediate-shim@npm:1.0.1" + checksum: 10/98c4d6778c98363436690a340077142ef11c1a8c8c6a78118242340c8e82bb2d66a1563707ef3f5455e7ff5cbee21c87e898b333cf3e7ed241bab12d19e9eab1 + languageName: node + linkType: hard + +"setimmediate@npm:^1.0.4, setimmediate@npm:^1.0.5": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" checksum: 10/76e3f5d7f4b581b6100ff819761f04a984fa3f3990e72a6554b57188ded53efce2d3d6c0932c10f810b7c59414f85e2ab3c11521877d1dea1ce0b56dc906f485 @@ -11448,6 +13036,13 @@ __metadata: languageName: node linkType: hard +"single-call-balance-checker-abi@npm:^1.0.0": + version: 1.0.0 + resolution: "single-call-balance-checker-abi@npm:1.0.0" + checksum: 10/d68fd83e58c42e6ccd4951d30cacffad01b4ca46595766b51ca09346f715027eac3a1d9e80e6147c7c20181ac6aca5d5330f9d1b131c91cbac3a16c1f3efbc3a + languageName: node + linkType: hard + "sirv@npm:^2.0.3": version: 2.0.4 resolution: "sirv@npm:2.0.4" @@ -11459,6 +13054,20 @@ __metadata: languageName: node linkType: hard +"siwe@npm:^2.3.2": + version: 2.3.2 + resolution: "siwe@npm:2.3.2" + dependencies: + "@spruceid/siwe-parser": "npm:^2.1.2" + "@stablelib/random": "npm:^1.0.1" + uri-js: "npm:^4.4.1" + valid-url: "npm:^1.0.9" + peerDependencies: + ethers: ^5.6.8 || ^6.0.8 + checksum: 10/6ea5ad9a9046fa916f85bf9d3092bc898f7e339d9c552714ea53ecc17daa4f78300c3cf7cc9c70fe57baf77dcee5cb38c6e1d692400b874cd84d297b1261918c + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -12181,6 +13790,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:^1.0.3": + version: 1.0.3 + resolution: "tweetnacl@npm:1.0.3" + checksum: 10/ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -12301,6 +13917,15 @@ __metadata: languageName: node linkType: hard +"ulid@npm:^2.3.0": + version: 2.4.0 + resolution: "ulid@npm:2.4.0" + bin: + ulid: bin/cli.js + checksum: 10/409ad0a703ea9f25d2a35cf790b767030a2c7b5ca0df6acb73669628156ab63bb97925a3fd7aaaf0b2384d39bd3ed2f7cd2c2de9f1171df6bc79b8c097865d92 + languageName: node + linkType: hard + "undici-types@npm:~6.19.2": version: 6.19.8 resolution: "undici-types@npm:6.19.8" @@ -12456,6 +14081,13 @@ __metadata: languageName: node linkType: hard +"utf8@npm:^3.0.0": + version: 3.0.0 + resolution: "utf8@npm:3.0.0" + checksum: 10/31d19c4faacbb65b09ebc1c21c32b20bdb0919c6f6773cee5001b99bb83f8e503e7233c08fc71ebb34f7cfebd95cec3243b81d90176097aa2f286cccb4ce866e + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -12883,6 +14515,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.18.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 + languageName: node + linkType: hard + "ws@npm:8.21.0": version: 8.21.0 resolution: "ws@npm:8.21.0" From 575e11db2fef8f8cfce4b654209080b94b2f6854 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Fri, 31 Jul 2026 00:55:18 +0100 Subject: [PATCH 2/8] feat: add Core messenger types and endowment Co-authored-by: Cursor --- packages/tron-wallet-snap/snap.manifest.json | 6 +++++ .../src/types/core-messenger.ts | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 packages/tron-wallet-snap/src/types/core-messenger.ts diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index e13773c8..d6e2e2af 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -48,6 +48,12 @@ }, "endowment:assets": { "scopes": ["tron:728126428"] + }, + "endowment:messenger": { + "actions": [ + "AssetsController:getAsset", + "RemoteFeatureFlagController:getState" + ] } }, "platformVersion": "11.2.0", diff --git a/packages/tron-wallet-snap/src/types/core-messenger.ts b/packages/tron-wallet-snap/src/types/core-messenger.ts new file mode 100644 index 00000000..b90518d2 --- /dev/null +++ b/packages/tron-wallet-snap/src/types/core-messenger.ts @@ -0,0 +1,24 @@ +import type { AssetsControllerGetAssetAction } from '@metamask/assets-controller'; +import type { Messenger } from '@metamask/messenger'; +import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; +import type { AsyncMessenger } from '@metamask/snaps-sdk'; + +export type { AssetsControllerGetAssetAction }; + +export type CoreMessengerActions = + | AssetsControllerGetAssetAction + | RemoteFeatureFlagControllerGetStateAction; + +/** + * Typed messenger for Core controller actions available to this Snap via + * `endowment:messenger` / `getMessenger`. + */ +export type CoreMessenger = Messenger; + +/** + * Narrow dependency for services that only need to invoke Core actions. + * + * Prefer this over injecting the full messenger so constructors only receive + * the surface they use. + */ +export type CoreMessengerCaller = Pick, 'call'>; From 9def87adbe6e627bb04f12f1178de6be84580e1c Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Fri, 31 Jul 2026 00:55:38 +0100 Subject: [PATCH 3/8] feat: add SNAP_OWNED_ASSETS and isSnapOwnedAsset Co-authored-by: Cursor --- .../src/clients/price-api/PriceApiClient.ts | 4 +-- .../src/clients/token-api/TokenApiClient.ts | 4 +-- .../tron-wallet-snap/src/constants/index.ts | 7 ++-- .../services/assets/snapOwnedAssets.test.ts | 32 +++++++++++++++++++ .../src/services/assets/snapOwnedAssets.ts | 18 +++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.test.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.ts diff --git a/packages/tron-wallet-snap/src/clients/price-api/PriceApiClient.ts b/packages/tron-wallet-snap/src/clients/price-api/PriceApiClient.ts index 4f42f0b8..4f7a92d0 100644 --- a/packages/tron-wallet-snap/src/clients/price-api/PriceApiClient.ts +++ b/packages/tron-wallet-snap/src/clients/price-api/PriceApiClient.ts @@ -7,7 +7,7 @@ import { mapKeys } from 'lodash'; import type { ICache } from '../../caching/ICache'; import { useCache } from '../../caching/useCache'; -import { SPECIAL_ASSETS } from '../../constants'; +import { SNAP_OWNED_ASSETS } from '../../constants'; import type { ConfigProvider } from '../../services/config'; import { buildUrl } from '../../utils/buildUrl'; import type { ILogger } from '../../utils/logger'; @@ -257,7 +257,7 @@ export class PriceApiClient { assert(vsCurrency, VsCurrencyParamStruct); const filteredTokens = tokenCaip19Types.filter( - (tokenCaip19Type) => !SPECIAL_ASSETS.includes(tokenCaip19Type), + (tokenCaip19Type) => !SNAP_OWNED_ASSETS.includes(tokenCaip19Type), ); return this.#getMultipleSpotPrices_CACHE(filteredTokens, vsCurrency); diff --git a/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts b/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts index 0d3c1c93..b40710fc 100644 --- a/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts +++ b/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts @@ -3,7 +3,7 @@ import { array, assert } from '@metamask/superstruct'; import type { Infer } from '@metamask/superstruct'; import { CaipAssetTypeStruct, parseCaipAssetType } from '@metamask/utils'; -import { Network, SPECIAL_ASSETS } from '../../constants'; +import { Network, SNAP_OWNED_ASSETS } from '../../constants'; import type { TokenCaipAssetType } from '../../services/assets/types'; import { TokenCaipAssetTypeStruct } from '../../services/assets/types'; import type { ConfigProvider } from '../../services/config'; @@ -94,7 +94,7 @@ export class TokenApiClient { * Exclude TRON resource tokens (energy and bandwidth), staked tokens, and tokens not from supported networks. */ const supportedAssetTypes = assetTypes.filter((assetType) => { - if (SPECIAL_ASSETS.includes(assetType)) { + if (SNAP_OWNED_ASSETS.includes(assetType)) { return false; } const { chainId } = parseCaipAssetType(assetType); diff --git a/packages/tron-wallet-snap/src/constants/index.ts b/packages/tron-wallet-snap/src/constants/index.ts index 7dbb15d0..4b46a8cf 100644 --- a/packages/tron-wallet-snap/src/constants/index.ts +++ b/packages/tron-wallet-snap/src/constants/index.ts @@ -389,7 +389,7 @@ export const Networks = { }, } as const; -export const SPECIAL_ASSETS: string[] = [ +export const SNAP_OWNED_ASSETS: string[] = [ KnownCaip19Id.TrxStakedForBandwidthMainnet, KnownCaip19Id.TrxStakedForBandwidthNile, KnownCaip19Id.TrxStakedForBandwidthShasta, @@ -419,9 +419,12 @@ export const SPECIAL_ASSETS: string[] = [ KnownCaip19Id.MaximumEnergyShasta, ]; +/** @deprecated Use {@link SNAP_OWNED_ASSETS} instead. */ +export const SPECIAL_ASSETS = SNAP_OWNED_ASSETS; + export const ESSENTIAL_ASSETS: string[] = [ KnownCaip19Id.TrxMainnet, KnownCaip19Id.TrxNile, KnownCaip19Id.TrxShasta, - ...SPECIAL_ASSETS, + ...SNAP_OWNED_ASSETS, ]; diff --git a/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.test.ts b/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.test.ts new file mode 100644 index 00000000..4de55df5 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.test.ts @@ -0,0 +1,32 @@ +import { + KnownCaip19Id, + SNAP_OWNED_ASSETS, +} from '../../constants'; +import { isSnapOwnedAsset } from './snapOwnedAssets'; + +describe('isSnapOwnedAsset', () => { + it.each(SNAP_OWNED_ASSETS)('returns true for snap-owned asset %s', (assetId) => { + expect(isSnapOwnedAsset(assetId)).toBe(true); + }); + + it('returns false for native TRX', () => { + expect(isSnapOwnedAsset(KnownCaip19Id.TrxMainnet)).toBe(false); + expect(isSnapOwnedAsset(KnownCaip19Id.TrxNile)).toBe(false); + expect(isSnapOwnedAsset(KnownCaip19Id.TrxShasta)).toBe(false); + }); + + it('returns false for TRC20 tokens', () => { + expect(isSnapOwnedAsset(KnownCaip19Id.UsdtMainnet)).toBe(false); + expect( + isSnapOwnedAsset( + `${KnownCaip19Id.TrxMainnet.split('/')[0]}/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`, + ), + ).toBe(false); + }); + + it('returns false for TRC10 tokens', () => { + expect( + isSnapOwnedAsset(`${KnownCaip19Id.TrxMainnet.split('/')[0]}/trc10:1002000`), + ).toBe(false); + }); +}); diff --git a/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.ts b/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.ts new file mode 100644 index 00000000..c8d44a17 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.ts @@ -0,0 +1,18 @@ +import { SNAP_OWNED_ASSETS } from '../../constants'; + +const SNAP_OWNED_ASSET_IDS = new Set(SNAP_OWNED_ASSETS); + +/** + * Returns whether an asset remains exclusively managed by the Snap. + * + * AssetsController does not persist certain Tron protocol assets, including + * staking positions and account resources. These assets must always be read, + * synchronized, persisted, and published by the Snap, regardless of the + * assets migration stage. + * + * @param assetId - CAIP-19 asset ID. + * @returns Whether the asset is exclusively managed by the Snap. + */ +export function isSnapOwnedAsset(assetId: string): boolean { + return SNAP_OWNED_ASSET_IDS.has(assetId); +} From 46679cc85851047e797cf103a907612fbda14682 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Fri, 31 Jul 2026 00:55:51 +0100 Subject: [PATCH 4/8] feat: add mapControllerAsset helper Co-authored-by: Cursor --- .../assets/mapControllerAsset.test.ts | 92 +++++++++++++++++++ .../src/services/assets/mapControllerAsset.ts | 39 ++++++++ 2 files changed, 131 insertions(+) create mode 100644 packages/tron-wallet-snap/src/services/assets/mapControllerAsset.test.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/mapControllerAsset.ts diff --git a/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.test.ts b/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.test.ts new file mode 100644 index 00000000..249469f2 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.test.ts @@ -0,0 +1,92 @@ +import type { Asset } from '@metamask/assets-controller'; +import type { CaipAssetType } from '@metamask/utils'; +import { parseCaipAssetType } from '@metamask/utils'; + +import { KnownCaip19Id, Network, TokenMetadata } from '../../constants'; +import type { AssetEntity } from '../../entities/assets'; +import { toUiAmount } from '../../utils/conversion'; + +import { mapControllerAsset } from './mapControllerAsset'; + +describe('mapControllerAsset', () => { + const accountId = 'account-id'; + const knownAssetId = KnownCaip19Id.TrxMainnet; + const unknownAssetId = 'tron:728126428/trc20:unknown'; + + it('maps controller metadata when present', () => { + const asset = { + id: unknownAssetId, + chainId: Network.Mainnet, + balance: { amount: '1234567' }, + metadata: { + type: 'fungible', + symbol: 'TKN', + name: 'Token', + decimals: 6, + image: 'https://example.com/token.png', + }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + } as unknown as Asset; + + expect(mapControllerAsset(accountId, unknownAssetId, asset)).toStrictEqual({ + assetType: unknownAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: 'TKN', + decimals: 6, + rawAmount: '1234567', + uiAmount: '1.234567', + iconUrl: 'https://example.com/token.png', + }); + }); + + it('falls back to TokenMetadata when controller metadata is missing', () => { + const asset = { + id: knownAssetId, + chainId: Network.Mainnet, + balance: { amount: '2000000' }, + metadata: { type: 'fungible', name: 'TRON' }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + } as unknown as Asset; + + expect( + mapControllerAsset(accountId, knownAssetId, asset), + ).toStrictEqual({ + assetType: knownAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: TokenMetadata[knownAssetId].symbol, + decimals: TokenMetadata[knownAssetId].decimals, + rawAmount: '2000000', + uiAmount: '2', + iconUrl: TokenMetadata[knownAssetId].iconUrl, + }); + }); + + it('uses empty defaults when metadata is missing everywhere', () => { + const assetId = 'tron:728126428/trc20:missing' as CaipAssetType; + const asset = { + id: assetId, + chainId: Network.Mainnet, + balance: { amount: '42' }, + metadata: { type: 'fungible', name: 'Missing' }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + } as unknown as Asset; + + const { chainId } = parseCaipAssetType(assetId); + + expect(mapControllerAsset(accountId, assetId, asset)).toStrictEqual({ + assetType: assetId, + keyringAccountId: accountId, + network: chainId, + symbol: '', + decimals: 0, + rawAmount: '42', + uiAmount: '42', + iconUrl: '', + }); + }); +}); diff --git a/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.ts b/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.ts new file mode 100644 index 00000000..24cdf276 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.ts @@ -0,0 +1,39 @@ +import type { Asset } from '@metamask/assets-controller'; +import type { CaipAssetType } from '@metamask/utils'; +import { parseCaipAssetType } from '@metamask/utils'; + +import { Network, TokenMetadata } from '../../constants'; +import type { AssetEntity } from '../../entities/assets'; +import { toUiAmount } from '../../utils/conversion'; + +/** + * Maps an AssetsController asset to the Snap's {@link AssetEntity} shape. + * + * @param accountId - Keyring account ID. + * @param assetId - CAIP-19 asset ID. + * @param asset - Asset returned by AssetsController. + * @returns Mapped asset entity. + */ +export function mapControllerAsset( + accountId: string, + assetId: string, + asset: Asset, +): AssetEntity { + const { chainId } = parseCaipAssetType(assetId as CaipAssetType); + const knownMetadata = TokenMetadata[assetId as keyof typeof TokenMetadata]; + const decimals = asset.metadata.decimals ?? knownMetadata?.decimals ?? 0; + const symbol = asset.metadata.symbol ?? knownMetadata?.symbol ?? ''; + const iconUrl = asset.metadata.image ?? knownMetadata?.iconUrl ?? ''; + const { amount } = asset.balance; + + return { + assetType: assetId, + keyringAccountId: accountId, + network: chainId as Network, + symbol, + decimals, + rawAmount: amount, + uiAmount: toUiAmount(amount, decimals).toString(), + iconUrl, + } as AssetEntity; +} From a586cc8c12bc76e482e664a9a650b9f745ac7c8b Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Fri, 31 Jul 2026 00:56:08 +0100 Subject: [PATCH 5/8] feat: add assets migration mode resolver Co-authored-by: Cursor --- packages/tron-wallet-snap/.env.example | 3 + packages/tron-wallet-snap/snap.config.ts | 1 + .../assets/assetsMigrationMode.test.ts | 164 ++++++++++++++++++ .../services/assets/assetsMigrationMode.ts | 129 ++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.test.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.ts diff --git a/packages/tron-wallet-snap/.env.example b/packages/tron-wallet-snap/.env.example index 1444ae2f..82be5095 100644 --- a/packages/tron-wallet-snap/.env.example +++ b/packages/tron-wallet-snap/.env.example @@ -29,3 +29,6 @@ TRONGRID_BASE_URL_SHASTA=https://shasta.api.trongrid.io TRON_HTTP_BASE_URL_MAINNET=https://api.trongrid.io TRON_HTTP_BASE_URL_NILE=https://nile.trongrid.io TRON_HTTP_BASE_URL_SHASTA=https://shasta.trongrid.io + +# Assets migration stage (non-production only): off|controller|0|2 +# TRON_ASSETS_MIGRATION_STAGE=off diff --git a/packages/tron-wallet-snap/snap.config.ts b/packages/tron-wallet-snap/snap.config.ts index ed7d1ef2..0ac872e3 100644 --- a/packages/tron-wallet-snap/snap.config.ts +++ b/packages/tron-wallet-snap/snap.config.ts @@ -35,6 +35,7 @@ const config: SnapConfig = { TRON_HTTP_BASE_URL_MAINNET: process.env.TRON_HTTP_BASE_URL_MAINNET ?? '', TRON_HTTP_BASE_URL_NILE: process.env.TRON_HTTP_BASE_URL_NILE ?? '', TRON_HTTP_BASE_URL_SHASTA: process.env.TRON_HTTP_BASE_URL_SHASTA ?? '', + TRON_ASSETS_MIGRATION_STAGE: process.env.TRON_ASSETS_MIGRATION_STAGE ?? '', }, polyfills: true, }; diff --git a/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.test.ts b/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.test.ts new file mode 100644 index 00000000..0e34e202 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.test.ts @@ -0,0 +1,164 @@ +import { + SNAPS_ASSETS_MIGRATION_FLAG_KEYS, + SnapsAssetsMigrationStage, +} from '@metamask/assets-controller'; + +import { Network } from '../../constants'; +import type { CoreMessengerCaller } from '../../types/core-messenger'; + +import { + modeFromStage, + parseStageFromEnv, + parseStageFromRemoteFlags, + resolveAssetsMigrationMode, +} from './assetsMigrationMode'; + +const TRON_CHAIN_ID = Network.Mainnet; +const TRON_FLAG_KEY = SNAPS_ASSETS_MIGRATION_FLAG_KEYS.tron; + +describe('assetsMigrationMode', () => { + describe('modeFromStage', () => { + it('maps Off to snap', () => { + expect(modeFromStage(SnapsAssetsMigrationStage.Off)).toBe('snap'); + }); + + it.each([ + SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback, + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + SnapsAssetsMigrationStage.ReadAssetsControllerOnly, + ])('maps active stage %s to controller', (stage) => { + expect(modeFromStage(stage)).toBe('controller'); + }); + }); + + describe('parseStageFromRemoteFlags', () => { + it('returns Off when remote flag is Off', () => { + expect( + parseStageFromRemoteFlags( + { [TRON_FLAG_KEY]: { stage: SnapsAssetsMigrationStage.Off } }, + TRON_CHAIN_ID, + ), + ).toBe(SnapsAssetsMigrationStage.Off); + }); + + it.each([ + SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback, + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + SnapsAssetsMigrationStage.ReadAssetsControllerOnly, + ])('returns stage %s from remote flags', (stage) => { + expect( + parseStageFromRemoteFlags( + { [TRON_FLAG_KEY]: { stage } }, + TRON_CHAIN_ID, + ), + ).toBe(stage); + }); + + it('returns undefined when remote flags are missing', () => { + expect(parseStageFromRemoteFlags(undefined, TRON_CHAIN_ID)).toBeUndefined(); + expect(parseStageFromRemoteFlags({}, TRON_CHAIN_ID)).toBeUndefined(); + }); + }); + + describe('parseStageFromEnv', () => { + it.each(['off', '0'])('accepts Off alias %s', (value) => { + expect(parseStageFromEnv(value)).toBe(SnapsAssetsMigrationStage.Off); + }); + + it.each(['read-assets-controller-without-fallback', '2', 'controller'])( + 'accepts controller alias %s', + (value) => { + expect(parseStageFromEnv(value)).toBe( + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + ); + }, + ); + + it.each([ + '1', + 'read-assets-controller-with-fallback', + '3', + 'read-assets-controller-only', + 'only', + 'with-fallback', + 'invalid', + ])('rejects unsupported env value %s', (value) => { + expect(parseStageFromEnv(value)).toBeUndefined(); + }); + }); + + describe('resolveAssetsMigrationMode', () => { + const createMessenger = ( + remoteFeatureFlags: Record | undefined, + ): CoreMessengerCaller => ({ + call: jest.fn().mockResolvedValue({ remoteFeatureFlags }), + }); + + it('prefers remote flags over env', async () => { + const coreMessenger = createMessenger({ + [TRON_FLAG_KEY]: { + stage: SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + }, + }); + + await expect( + resolveAssetsMigrationMode({ + coreMessenger, + chainId: TRON_CHAIN_ID, + isDev: true, + envStage: SnapsAssetsMigrationStage.Off, + }), + ).resolves.toBe('controller'); + }); + + it('uses env in non-production when remote flags are absent', async () => { + const coreMessenger = createMessenger({}); + + await expect( + resolveAssetsMigrationMode({ + coreMessenger, + chainId: TRON_CHAIN_ID, + isDev: true, + envStage: + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + }), + ).resolves.toBe('controller'); + + await expect( + resolveAssetsMigrationMode({ + coreMessenger, + chainId: TRON_CHAIN_ID, + isDev: true, + envStage: SnapsAssetsMigrationStage.Off, + }), + ).resolves.toBe('snap'); + }); + + it('ignores env in production', async () => { + const coreMessenger = createMessenger({}); + + await expect( + resolveAssetsMigrationMode({ + coreMessenger, + chainId: TRON_CHAIN_ID, + isDev: false, + envStage: + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + }), + ).resolves.toBe('snap'); + }); + + it('defaults to snap when everything is missing', async () => { + const coreMessenger = createMessenger(undefined); + + await expect( + resolveAssetsMigrationMode({ + coreMessenger, + chainId: TRON_CHAIN_ID, + isDev: true, + envStage: undefined, + }), + ).resolves.toBe('snap'); + }); + }); +}); diff --git a/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.ts b/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.ts new file mode 100644 index 00000000..accefc64 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.ts @@ -0,0 +1,129 @@ +import { + SNAPS_ASSETS_MIGRATION_FLAG_KEYS, + SnapsAssetsMigrationStage, + getSnapsAssetsMigrationNamespace, + parseSnapsAssetsMigrationStage, +} from '@metamask/assets-controller'; +import type { Json } from '@metamask/utils'; + +import { Network } from '../../constants'; +import type { CoreMessengerCaller } from '../../types/core-messenger'; + +export type AssetsMigrationMode = 'snap' | 'controller'; + +const ENV_TO_STAGE: Record = { + off: SnapsAssetsMigrationStage.Off, + '0': SnapsAssetsMigrationStage.Off, + 'read-assets-controller-without-fallback': + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + '2': SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + controller: SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, +}; + +/** + * Collapse Core stages into the two Snap outcomes. + * + * @param stage - Resolved migration stage. + * @returns `'snap'` when Off, otherwise `'controller'`. + */ +export function modeFromStage( + stage: SnapsAssetsMigrationStage, +): AssetsMigrationMode { + return stage === SnapsAssetsMigrationStage.Off ? 'snap' : 'controller'; +} + +/** + * Extract the migration stage for a chain from remote feature flags. + * + * @param remoteFeatureFlags - Remote feature flag payload from the wallet. + * @param chainId - CAIP-2 chain id for the network. + * @returns The configured stage, or `undefined` when the flag key is absent. + */ +export function parseStageFromRemoteFlags( + remoteFeatureFlags: Record | undefined, + chainId: string, +): SnapsAssetsMigrationStage | undefined { + if (!remoteFeatureFlags) { + return undefined; + } + + const namespace = getSnapsAssetsMigrationNamespace(chainId); + if (!namespace) { + return undefined; + } + + const flagKey = SNAPS_ASSETS_MIGRATION_FLAG_KEYS[namespace]; + if (!(flagKey in remoteFeatureFlags)) { + return undefined; + } + + return parseSnapsAssetsMigrationStage( + remoteFeatureFlags[flagKey] as Json | undefined, + ); +} + +/** + * Parse a raw `TRON_ASSETS_MIGRATION_STAGE` env value. + * + * Only Off and WithoutFallback aliases are accepted. WithFallback and Only + * strings are rejected so they cannot force controller mode without a remote flag. + * + * @param rawStage - Raw env value, possibly undefined. + * @returns The configured stage, or `undefined` when unset or invalid. + */ +export function parseStageFromEnv( + rawStage: string | undefined, +): SnapsAssetsMigrationStage | undefined { + const normalized = rawStage?.trim().toLowerCase(); + + if (normalized && normalized in ENV_TO_STAGE) { + return ENV_TO_STAGE[normalized]; + } + + return undefined; +} + +/** + * Resolve the assets migration mode for a network. + * + * Resolution order: + * 1. Remote feature flags + * 2. `TRON_ASSETS_MIGRATION_STAGE` in non-production builds + * 3. `'snap'` + * + * @param params - Resolution inputs. + * @param params.coreMessenger - Messenger used to read remote flags. + * @param params.chainId - CAIP-2 chain id for the network. + * @param params.isDev - Whether the Snap is running in a non-production build. + * @param params.envStage - Parsed env stage override. + * @returns The resolved migration mode. + */ +export async function resolveAssetsMigrationMode({ + coreMessenger, + chainId, + isDev, + envStage, +}: { + coreMessenger: CoreMessengerCaller; + chainId: string; + isDev: boolean; + envStage: SnapsAssetsMigrationStage | undefined; +}): Promise { + const state = await coreMessenger.call('RemoteFeatureFlagController:getState'); + const remoteStage = parseStageFromRemoteFlags( + state.remoteFeatureFlags, + chainId, + ); + + if (remoteStage !== undefined) { + return modeFromStage(remoteStage); + } + + if (isDev && envStage !== undefined) { + return modeFromStage(envStage); + } + + return 'snap'; +} + +export { SnapsAssetsMigrationStage }; From 1113cb2dfe6dc7fc3bba2344b13359793cdcdbe1 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Fri, 31 Jul 2026 00:56:26 +0100 Subject: [PATCH 6/8] feat: wire Core messenger into AssetsService context Co-authored-by: Cursor --- packages/tron-wallet-snap/src/context.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tron-wallet-snap/src/context.ts b/packages/tron-wallet-snap/src/context.ts index 029ec8eb..76890dcf 100644 --- a/packages/tron-wallet-snap/src/context.ts +++ b/packages/tron-wallet-snap/src/context.ts @@ -1,3 +1,5 @@ +import { getMessenger } from '@metamask/snaps-sdk'; + import { InMemoryCache } from './caching/InMemoryCache'; import { StateCache } from './caching/StateCache'; import { PriceApiClient } from './clients/price-api/PriceApiClient'; @@ -29,6 +31,7 @@ import { TransactionScanService } from './services/transaction-scan/TransactionS import { TransactionsRepository } from './services/transactions/TransactionsRepository'; import { TransactionsService } from './services/transactions/TransactionsService'; import { WalletService } from './services/wallet/WalletService'; +import type { CoreMessenger } from './types/core-messenger'; import logger, { noOpLogger } from './utils/logger'; /** @@ -82,6 +85,8 @@ const priceApiClient = new PriceApiClient(configProvider, priceCache); // Token API client const tokenApiClient = new TokenApiClient(configProvider); +const coreMessenger = getMessenger(); + // Security Alerts API client const securityAlertsApiClient = new SecurityAlertsApiClient( configProvider, @@ -98,6 +103,7 @@ const assetsService = new AssetsService({ priceApiClient, tokenApiClient, snapClient, + coreMessenger, }); const transactionsService = new TransactionsService({ From 22b868c16251f1ddcb87976227ea1fbdb8cffad9 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Fri, 31 Jul 2026 00:57:53 +0100 Subject: [PATCH 7/8] feat: route AssetsService fungibles via feature flag Co-authored-by: Cursor --- packages/tron-wallet-snap/CHANGELOG.md | 4 + packages/tron-wallet-snap/snap.manifest.json | 2 +- .../src/services/assets/AssetsService.test.ts | 388 +++++++++++++++++- .../src/services/assets/AssetsService.ts | 184 +++++++-- 4 files changed, 548 insertions(+), 30 deletions(-) diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 71db1a30..4ceb03cd 100644 --- a/packages/tron-wallet-snap/CHANGELOG.md +++ b/packages/tron-wallet-snap/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add feature-flagged AssetsController migration for fungible asset reads (TRX, TRC10, TRC20). Protocol assets (energy, bandwidth, staking, lock/withdrawal, rewards) remain Snap-owned. Resolution order: remote feature flags → `TRON_ASSETS_MIGRATION_STAGE` env (non-production only) → Off default. + ### Changed - Bump `@metamask/keyring-api` from `^23.2.0` to `^23.7.0` ([#43](https://github.com/MetaMask/internal-snaps/pull/43)) diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index d6e2e2af..e1b9d480 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "tcnp/6xxntfergEzeQYc7k96SfveVo+5kBQHGEXqZkQ=", + "shasum": "HT62C2CzaOh8bzpnWeK/LO9uqW+lcxs+RXnHzaUOXRA=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts b/packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts index b41e8c7d..809ae7e2 100644 --- a/packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts +++ b/packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts @@ -1,3 +1,8 @@ +import type { Asset } from '@metamask/assets-controller'; +import { + SNAPS_ASSETS_MIGRATION_FLAG_KEYS, + SnapsAssetsMigrationStage, +} from '@metamask/assets-controller'; import type { KeyringAccount } from '@metamask/keyring-api'; import { KeyringEvent } from '@metamask/keyring-api'; import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; @@ -10,8 +15,9 @@ import type { AccountResources, TronHttpClient } from '../../clients/tron-http'; import { TrongridAccountNotFoundError } from '../../clients/trongrid/errors'; import type { TrongridApiClient } from '../../clients/trongrid/TrongridApiClient'; import type { Trc20Balance, TronAccount } from '../../clients/trongrid/types'; -import { KnownCaip19Id, Network } from '../../constants'; +import { KnownCaip19Id, Network, SNAP_OWNED_ASSETS } from '../../constants'; import type { AssetEntity } from '../../entities/assets'; +import type { CoreMessengerCaller } from '../../types/core-messenger'; import { mockLogger } from '../../utils/mockLogger'; import type { AssetsRepository } from './AssetsRepository'; import type { NativeCaipAssetType, TokenCaipAssetType } from './types'; @@ -51,6 +57,67 @@ jest.mock('@metamask/keyring-snap-sdk', () => ({ // eslint-disable-next-line @typescript-eslint/no-require-imports const { AssetsService } = require('./AssetsService'); +const TRON_FLAG_KEY = SNAPS_ASSETS_MIGRATION_FLAG_KEYS.tron; + +function createMessengerCallMock( + getState: () => unknown, + getAsset: jest.Mock, +): (method: string, ...args: unknown[]) => Promise { + return async (method: string, ...args: unknown[]) => { + switch (method) { + case 'RemoteFeatureFlagController:getState': + return getState(); + case 'AssetsController:getAsset': + return getAsset(...args); + default: + return undefined; + } + }; +} + +function restoreMigrationStageEnv( + originalEnvironment: string | undefined, + originalStage: string | undefined, +): void { + /* eslint-disable no-restricted-globals */ + if (originalEnvironment === undefined) { + delete process.env.ENVIRONMENT; + } else { + process.env.ENVIRONMENT = originalEnvironment; + } + delete process.env.TRON_ASSETS_MIGRATION_STAGE; + if (originalStage !== undefined) { + process.env.TRON_ASSETS_MIGRATION_STAGE = originalStage; + } + /* eslint-enable no-restricted-globals */ +} + +function buildControllerAsset( + assetId: string, + amount: string, + metadata: { + symbol: string; + name: string; + decimals: number; + image?: string; + }, +): Asset { + return { + id: assetId as Asset['id'], + chainId: Network.Mainnet as Asset['chainId'], + balance: { amount }, + metadata: { + type: 'fungible', + symbol: metadata.symbol, + name: metadata.name, + decimals: metadata.decimals, + image: metadata.image, + }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + } as Asset; +} + const mockAccount: KeyringAccount = { id: 'test-account-id', address: 'TGJn1wnUYHJbvN88cynZbsAz2EMeZq73yx', @@ -187,6 +254,8 @@ type WithAssetsServiceCallback = (payload: { >; mockTokenApiClient: jest.Mocked>; mockSnapClient: jest.Mocked>; + mockCoreMessenger: jest.Mocked; + setMigrationStage: (stage: SnapsAssetsMigrationStage) => void; }) => Promise | ReturnValue; /** @@ -259,6 +328,25 @@ async function withAssetsService( trackError: jest.fn().mockResolvedValue(undefined), }; + let migrationStage = SnapsAssetsMigrationStage.Off; + const mockGetAsset = jest.fn(); + const mockCoreMessenger: jest.Mocked = { + call: jest.fn().mockImplementation( + createMessengerCallMock( + () => ({ + remoteFeatureFlags: { + [TRON_FLAG_KEY]: { stage: migrationStage }, + }, + }), + mockGetAsset, + ), + ), + }; + + const setMigrationStage = (stage: SnapsAssetsMigrationStage): void => { + migrationStage = stage; + }; + const assetsService = new AssetsService({ logger: mockLogger, assetsRepository: mockAssetsRepository, @@ -268,6 +356,7 @@ async function withAssetsService( priceApiClient: mockPriceApiClient, tokenApiClient: mockTokenApiClient, snapClient: mockSnapClient, + coreMessenger: mockCoreMessenger, }); return await testFunction({ @@ -279,6 +368,8 @@ async function withAssetsService( mockPriceApiClient, mockTokenApiClient, mockSnapClient, + mockCoreMessenger, + setMigrationStage, }); } @@ -2830,4 +2921,299 @@ describe('AssetsService', () => { }); }); }); + + describe('assets migration mode', () => { + const accountId = mockAccount.id; + const fungibleAssetId = KnownCaip19Id.TrxMainnet; + const snapAssetId = KnownCaip19Id.EnergyMainnet; + + it('fetchAssetsAndBalancesForAccount returns fungibles when mode is snap', async () => { + await withAssetsService( + async ({ + assetsService, + mockTrongridApiClient, + mockTronHttpClient, + setMigrationStage, + }) => { + setMigrationStage(SnapsAssetsMigrationStage.Off); + mockTrongridApiClient.getAccountInfoByAddress.mockResolvedValue( + createMockTronAccount({ + address: mockAccount.address, + balance: 1_000_000, + }), + ); + mockTronHttpClient.getAccountResources.mockResolvedValue( + emptyAccountResources, + ); + + const assets = await assetsService.fetchAssetsAndBalancesForAccount( + Network.Mainnet, + mockAccount, + ); + + expect( + assets.some((asset) => asset.assetType === fungibleAssetId), + ).toBe(true); + }, + ); + }); + + it('fetchAssetsAndBalancesForAccount returns protocol assets only when mode is controller', async () => { + await withAssetsService( + async ({ + assetsService, + mockTrongridApiClient, + mockTronHttpClient, + setMigrationStage, + }) => { + setMigrationStage( + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + ); + mockTrongridApiClient.getAccountInfoByAddress.mockResolvedValue( + createMockTronAccount({ + address: mockAccount.address, + balance: 1_000_000, + trc20: [{ TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t: '1000' }], + }), + ); + mockTronHttpClient.getAccountResources.mockResolvedValue( + emptyAccountResources, + ); + + const assets = await assetsService.fetchAssetsAndBalancesForAccount( + Network.Mainnet, + mockAccount, + ); + + expect( + assets.every((asset) => SNAP_OWNED_ASSETS.includes(asset.assetType)), + ).toBe(true); + expect( + assets.some((asset) => asset.assetType === fungibleAssetId), + ).toBe(false); + }, + ); + }); + + it('routes snap-owned reads through the repository', async () => { + await withAssetsService( + async ({ + assetsService, + mockAssetsRepository, + mockCoreMessenger, + setMigrationStage, + }) => { + setMigrationStage( + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + ); + const snapAsset: AssetEntity = { + assetType: snapAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: 'ENERGY', + decimals: 0, + rawAmount: '100', + uiAmount: '100', + iconUrl: '', + }; + mockAssetsRepository.getByAccountIdAndAssetType.mockResolvedValue( + snapAsset, + ); + + const asset = await assetsService.getAssetByAccountId( + accountId, + snapAssetId, + ); + + expect(asset).toStrictEqual(snapAsset); + expect(mockCoreMessenger.call).not.toHaveBeenCalledWith( + 'AssetsController:getAsset', + expect.anything(), + expect.anything(), + ); + }, + ); + }); + + it('routes fungible reads through AssetsController when mode is controller', async () => { + await withAssetsService( + async ({ + assetsService, + mockCoreMessenger, + setMigrationStage, + }) => { + setMigrationStage( + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + ); + mockCoreMessenger.call.mockImplementation( + createMessengerCallMock( + () => ({ + remoteFeatureFlags: { + [TRON_FLAG_KEY]: { + stage: + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + }, + }, + }), + jest.fn().mockResolvedValue( + buildControllerAsset(fungibleAssetId, '2000000', { + symbol: 'TRX', + name: 'TRON', + decimals: 6, + }), + ), + ), + ); + + const asset = await assetsService.getAssetByAccountId( + accountId, + fungibleAssetId, + ); + + expect(asset).toMatchObject({ + assetType: fungibleAssetId, + rawAmount: '2000000', + uiAmount: '2', + }); + }, + ); + }); + + it('getByKeyringAccountId excludes fungibles when mode is controller', async () => { + await withAssetsService( + async ({ + assetsService, + mockAssetsRepository, + setMigrationStage, + }) => { + setMigrationStage( + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + ); + mockAssetsRepository.getByAccountId.mockResolvedValue([ + { + assetType: fungibleAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: 'TRX', + decimals: 6, + rawAmount: '1000000', + uiAmount: '1', + iconUrl: '', + }, + { + assetType: snapAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: 'ENERGY', + decimals: 0, + rawAmount: '100', + uiAmount: '100', + iconUrl: '', + }, + ]); + + const assets = await assetsService.getByKeyringAccountId(accountId); + + expect(assets.some((asset) => asset.assetType === fungibleAssetId)).toBe( + false, + ); + expect(assets.some((asset) => asset.assetType === snapAssetId)).toBe( + true, + ); + }, + ); + }); + + it('saveMany emits only snap-owned assets when mode is controller', async () => { + await withAssetsService( + async ({ + assetsService, + mockState, + setMigrationStage, + }) => { + setMigrationStage( + SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, + ); + mockState.getKey.mockResolvedValue({}); + + const assets: AssetEntity[] = [ + { + assetType: fungibleAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: 'TRX', + decimals: 6, + rawAmount: '1000000', + uiAmount: '1', + iconUrl: '', + }, + { + assetType: snapAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: 'ENERGY', + decimals: 0, + rawAmount: '100', + uiAmount: '100', + iconUrl: '', + }, + ]; + + await assetsService.saveMany(assets); + + expect(emitSnapKeyringEvent).toHaveBeenCalledWith( + expect.anything(), + KeyringEvent.AccountAssetListUpdated, + { + assets: { + [accountId]: { + added: [snapAssetId], + removed: [], + }, + }, + }, + ); + }, + ); + }); + + it('ignores TRON_ASSETS_MIGRATION_STAGE in production', async () => { + /* eslint-disable no-restricted-globals */ + const originalEnvironment = process.env.ENVIRONMENT; + const originalStage = process.env.TRON_ASSETS_MIGRATION_STAGE; + process.env.ENVIRONMENT = 'production'; + process.env.TRON_ASSETS_MIGRATION_STAGE = '2'; + /* eslint-enable no-restricted-globals */ + + await withAssetsService( + async ({ assetsService, mockAssetsRepository, mockCoreMessenger }) => { + mockCoreMessenger.call.mockImplementation( + createMessengerCallMock(() => ({ remoteFeatureFlags: {} }), jest.fn()), + ); + const snapAsset: AssetEntity = { + assetType: fungibleAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: 'TRX', + decimals: 6, + rawAmount: '1000000', + uiAmount: '1', + iconUrl: '', + }; + mockAssetsRepository.getByAccountIdAndAssetType.mockResolvedValue( + snapAsset, + ); + + const asset = await assetsService.getAssetByAccountId( + accountId, + fungibleAssetId, + ); + + expect(asset).toStrictEqual(snapAsset); + }, + ); + + restoreMigrationStageEnv(originalEnvironment, originalStage); + }); + }); }); diff --git a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts index 4675a349..24e733d1 100644 --- a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts +++ b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts @@ -60,7 +60,15 @@ import { toUiAmount } from '../../utils/conversion'; import { createPrefixedLogger } from '../../utils/logger'; import type { ILogger } from '../../utils/logger'; import type { State, UnencryptedStateValue } from '../state/State'; +import type { CoreMessengerCaller } from '../../types/core-messenger'; import type { AssetsRepository } from './AssetsRepository'; +import type { AssetsMigrationMode } from './assetsMigrationMode'; +import { + parseStageFromEnv, + resolveAssetsMigrationMode, +} from './assetsMigrationMode'; +import { mapControllerAsset } from './mapControllerAsset'; +import { isSnapOwnedAsset } from './snapOwnedAssets'; import type { InLockPeriodCaipAssetType, NativeCaipAssetType, @@ -113,6 +121,12 @@ export class AssetsService { readonly #snapClient: SnapClient; + readonly #coreMessenger: CoreMessengerCaller; + + readonly #isDev: boolean; + + readonly #envMigrationStage: ReturnType; + readonly cacheTtlsMilliseconds: { fiatExchangeRates: number; spotPrices: number; @@ -128,6 +142,7 @@ export class AssetsService { priceApiClient, tokenApiClient, snapClient, + coreMessenger, }: { logger: ILogger; assetsRepository: AssetsRepository; @@ -137,6 +152,7 @@ export class AssetsService { priceApiClient: PriceApiClient; tokenApiClient: TokenApiClient; snapClient: SnapClient; + coreMessenger: CoreMessengerCaller; }) { this.#logger = createPrefixedLogger(logger, '[🪙 AssetsService]'); this.#assetsRepository = assetsRepository; @@ -146,6 +162,12 @@ export class AssetsService { this.#priceApiClient = priceApiClient; this.#tokenApiClient = tokenApiClient; this.#snapClient = snapClient; + this.#coreMessenger = coreMessenger; + // Static env access so the Snap bundler inlines values at build time. + this.#isDev = process.env.ENVIRONMENT !== 'production'; + this.#envMigrationStage = parseStageFromEnv( + process.env.TRON_ASSETS_MIGRATION_STAGE, + ); const { cacheTtlsMilliseconds } = configProvider.get().priceApi; this.cacheTtlsMilliseconds = cacheTtlsMilliseconds; @@ -155,6 +177,15 @@ export class AssetsService { return caipAssetId.includes('swift:0/iso4217:'); } + async #resolveMode(chainId: string): Promise { + return resolveAssetsMigrationMode({ + coreMessenger: this.#coreMessenger, + chainId, + isDev: this.#isDev, + envStage: this.#envMigrationStage, + }); + } + async getAllAssetsByAccountId(accountId: string): Promise { return this.#assetsRepository.getByAccountId(accountId); } @@ -163,9 +194,10 @@ export class AssetsService { accountId: string, assetTypes: string[], ): Promise<(AssetEntity | null)[]> { - return this.#assetsRepository.getByAccountIdAndAssetTypes( - accountId, - assetTypes, + return Promise.all( + assetTypes.map(async (assetType) => + this.getAssetByAccountId(accountId, assetType), + ), ); } @@ -173,6 +205,30 @@ export class AssetsService { accountId: string, assetType: string, ): Promise { + if (isSnapOwnedAsset(assetType)) { + return this.#assetsRepository.getByAccountIdAndAssetType( + accountId, + assetType, + ); + } + + const { chainId } = parseCaipAssetType(assetType as CaipAssetType); + const mode = await this.#resolveMode(chainId); + + if (mode === 'controller') { + const result = await this.#coreMessenger.call( + 'AssetsController:getAsset', + accountId, + assetType, + ); + + if (!result) { + return null; + } + + return mapControllerAsset(accountId, assetType, result); + } + return this.#assetsRepository.getByAccountIdAndAssetType( accountId, assetType, @@ -208,6 +264,8 @@ export class AssetsService { scope, }); + const mode = await this.#resolveMode(scope); + const [ tronAccountInfoRequest, tronAccountResourcesRequest, @@ -226,18 +284,19 @@ export class AssetsService { ); } - const trc20BalancesFallback = isInactiveAccount - ? await this.#trongridApiClient - .getTrc20BalancesByAddress(scope, account.address) - .catch(async (error) => { - await this.#snapClient.trackError(error as Error); - this.#logger.warn( - 'Failed to fetch TRC20 balances for inactive account', - { error, account, scope }, - ); - return []; - }) - : []; + const trc20BalancesFallback = + mode === 'snap' && isInactiveAccount + ? await this.#trongridApiClient + .getTrc20BalancesByAddress(scope, account.address) + .catch(async (error) => { + await this.#snapClient.trackError(error as Error); + this.#logger.warn( + 'Failed to fetch TRC20 balances for inactive account', + { error, account, scope }, + ); + return []; + }) + : []; const accountData = this.#buildAccountData({ tronAccountInfoRequest, @@ -246,7 +305,10 @@ export class AssetsService { stakingRewardsRequest, }); - const rawAssets = this.#extractAssets(account, scope, accountData); + const rawAssets = + mode === 'controller' + ? this.#extractSnapOwnedAssets(account, scope, accountData) + : this.#extractAssets(account, scope, accountData); const assetTypes = rawAssets.map((asset) => asset.assetType); const priceableAssetTypes = this.#getPriceableAssetTypes(rawAssets); @@ -371,12 +433,22 @@ export class AssetsService { ): AssetEntity[] { return [ this.#extractNativeAsset(account, scope, data.nativeBalance), + ...this.#extractSnapOwnedAssets(account, scope, data), + ...this.#extractTrc10Assets(account, scope, data.trc10Balances), + ...this.#extractTrc20Assets(account, scope, data.trc20Balances), + ]; + } + + #extractSnapOwnedAssets( + account: KeyringAccount, + scope: Network, + data: NormalizedAccountData, + ): AssetEntity[] { + return [ ...this.#extractStakedNativeAssets(account, scope, data.stakedData), this.#extractReadyForWithdrawalAsset(account, scope, data.stakedData), this.#extractInLockPeriodAsset(account, scope, data.stakedData), this.#extractStakingRewardsAsset(account, scope, data.stakingRewards), - ...this.#extractTrc10Assets(account, scope, data.trc10Balances), - ...this.#extractTrc20Assets(account, scope, data.trc20Balances), ...this.#extractBandwidth({ account, scope, @@ -1261,6 +1333,19 @@ export class AssetsService { async saveMany(assets: AssetEntity[]): Promise { this.#logger.info('Saving assets', assets); + const modesByNetwork = new Map(); + await Promise.all( + [...new Set(assets.map((asset) => asset.network))].map( + async (network) => { + modesByNetwork.set(network, await this.#resolveMode(network)); + }, + ), + ); + + const shouldEmitAsset = (asset: AssetEntity): boolean => + (modesByNetwork.get(asset.network) ?? 'snap') === 'snap' || + isSnapOwnedAsset(asset.assetType); + const hasZeroAmount = (asset: AssetEntity): boolean => asset.rawAmount === '0' || asset.uiAmount === '0'; @@ -1268,6 +1353,13 @@ export class AssetsService { const isEssentialAsset = (asset: AssetEntity): boolean => ESSENTIAL_ASSETS.includes(asset.assetType); + const isProtectedAsset = (asset: AssetEntity): boolean => { + const mode = modesByNetwork.get(asset.network) ?? 'snap'; + return mode === 'snap' + ? isEssentialAsset(asset) + : isSnapOwnedAsset(asset.assetType); + }; + // Track only the account/network pairs refreshed in this run. // That prevents us from treating assets from untouched networks as disappeared. const syncedNetworksByAccount = assets.reduce>>( @@ -1292,11 +1384,16 @@ export class AssetsService { if ( !syncedNetworks?.has(savedAsset.network) || - isEssentialAsset(savedAsset) + isProtectedAsset(savedAsset) ) { return false; } + const mode = modesByNetwork.get(savedAsset.network) ?? 'snap'; + if (mode === 'controller' && !isSnapOwnedAsset(savedAsset.assetType)) { + return false; + } + return !incomingAssetKeys.has( `${savedAsset.keyringAccountId}:${savedAsset.assetType}`, ); @@ -1320,9 +1417,9 @@ export class AssetsService { // snapshot entirely // 2. fold in the current assets to report additions and explicit zero-balance // removals in the same event - const assetListUpdatedPayload = disappearedAssets.reduce< - AccountAssetListUpdatedEvent['params']['assets'] - >( + const assetListUpdatedPayload = disappearedAssets + .filter(shouldEmitAsset) + .reduce( (acc, asset) => ({ ...acc, [asset.keyringAccountId]: { @@ -1336,7 +1433,7 @@ export class AssetsService { {}, ); - for (const asset of assets) { + for (const asset of assets.filter(shouldEmitAsset)) { // Merge the current snapshot into the pre-seeded payload so each account // ends up with one consolidated added/removed diff. assetListUpdatedPayload[asset.keyringAccountId] = { @@ -1367,7 +1464,9 @@ export class AssetsService { // Emit synthetic zero-balance entries for disappeared assets so clients can // clear cached balances even when the backend omits zero-balance tokens // instead of returning them explicitly. - const removedAssetsWithZeroBalance = disappearedAssets.map((asset) => ({ + const removedAssetsWithZeroBalance = disappearedAssets + .filter(shouldEmitAsset) + .map((asset) => ({ ...asset, rawAmount: '0', uiAmount: '0', @@ -1379,9 +1478,10 @@ export class AssetsService { // Broadcast the current snapshot plus synthetic zero-balance removals so the // client can reconcile both visible assets and cached balances in one pass. - const balancesUpdatedPayload = assetsToSave.reduce< - AccountBalancesUpdatedEvent['params']['balances'] - >( + const balancesUpdatedPayload = [ + ...assets.filter(shouldEmitAsset), + ...removedAssetsWithZeroBalance, + ].reduce( (acc, asset) => ({ ...acc, [asset.keyringAccountId]: { @@ -1448,6 +1548,27 @@ export class AssetsService { const savedAssets = await this.#assetsRepository.getByAccountId(keyringAccountId); + const networks = [ + ...new Set([ + ...savedAssets.map((asset) => asset.network), + ...ESSENTIAL_ASSETS.map( + (assetId) => + parseCaipAssetType(assetId as CaipAssetType).chainId as Network, + ), + ]), + ] as Network[]; + const modesByNetwork = new Map(); + await Promise.all( + networks.map(async (network) => { + modesByNetwork.set(network, await this.#resolveMode(network)); + }), + ); + + const visibleSavedAssets = savedAssets.filter((asset) => { + const mode = modesByNetwork.get(asset.network) ?? 'snap'; + return mode === 'snap' || isSnapOwnedAsset(asset.assetType); + }); + /** * Ensure the special assets are always present whether they have been synced or not. * These are assets that should be visible to the user even with zero balance. @@ -1455,6 +1576,13 @@ export class AssetsService { const missingEssentialAssets: AssetEntity[] = []; for (const essentialAssetId of ESSENTIAL_ASSETS) { + const { chainId } = parseCaipAssetType(essentialAssetId as CaipAssetType); + const mode = modesByNetwork.get(chainId as Network) ?? 'snap'; + + if (mode === 'controller' && !isSnapOwnedAsset(essentialAssetId)) { + continue; + } + const savedAsset = savedAssets.find( (asset) => (asset.assetType as string) === essentialAssetId, ); @@ -1468,7 +1596,7 @@ export class AssetsService { } } - return [...savedAssets, ...missingEssentialAssets]; + return [...visibleSavedAssets, ...missingEssentialAssets]; } /** From 334476d3cc9a884fe9f856bd5fc64c839f462074 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Fri, 31 Jul 2026 15:32:25 +0100 Subject: [PATCH 8/8] refactor(tron-wallet-snap): split assets reads into Core and Snap adapters Move legacy assets logic into SnapAssetsAdapter, add CoreAssetsAdapter for AssetsController reads, and keep AssetsService as a thin facade with inline stage routing. Resolve migration stage from remote flags in the Snap with a hardcoded fallback constant instead of a separate mode module or env vars. --- packages/tron-wallet-snap/.env.example | 3 - packages/tron-wallet-snap/snap.config.ts | 1 - packages/tron-wallet-snap/snap.manifest.json | 2 +- .../handlers/clientRequest/clientRequest.ts | 51 +- .../tron-wallet-snap/src/handlers/keyring.ts | 28 +- .../src/services/assets/AssetsService.ts | 2011 ++--------------- .../assets/adapters/CoreAssetsAdapter.ts | 99 + .../assets/adapters/SnapAssetsAdapter.ts | 1914 ++++++++++++++++ .../assets/assetsMigrationMode.test.ts | 164 -- .../services/assets/assetsMigrationMode.ts | 129 -- .../confirmation/ConfirmationHandler.ts | 7 +- .../src/services/send/SendService.ts | 12 +- .../src/types/core-messenger.ts | 11 +- .../views/ConfirmSignTransaction/render.tsx | 7 +- 14 files changed, 2264 insertions(+), 2175 deletions(-) create mode 100644 packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts delete mode 100644 packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.test.ts delete mode 100644 packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.ts diff --git a/packages/tron-wallet-snap/.env.example b/packages/tron-wallet-snap/.env.example index 82be5095..1444ae2f 100644 --- a/packages/tron-wallet-snap/.env.example +++ b/packages/tron-wallet-snap/.env.example @@ -29,6 +29,3 @@ TRONGRID_BASE_URL_SHASTA=https://shasta.api.trongrid.io TRON_HTTP_BASE_URL_MAINNET=https://api.trongrid.io TRON_HTTP_BASE_URL_NILE=https://nile.trongrid.io TRON_HTTP_BASE_URL_SHASTA=https://shasta.trongrid.io - -# Assets migration stage (non-production only): off|controller|0|2 -# TRON_ASSETS_MIGRATION_STAGE=off diff --git a/packages/tron-wallet-snap/snap.config.ts b/packages/tron-wallet-snap/snap.config.ts index 0ac872e3..ed7d1ef2 100644 --- a/packages/tron-wallet-snap/snap.config.ts +++ b/packages/tron-wallet-snap/snap.config.ts @@ -35,7 +35,6 @@ const config: SnapConfig = { TRON_HTTP_BASE_URL_MAINNET: process.env.TRON_HTTP_BASE_URL_MAINNET ?? '', TRON_HTTP_BASE_URL_NILE: process.env.TRON_HTTP_BASE_URL_NILE ?? '', TRON_HTTP_BASE_URL_SHASTA: process.env.TRON_HTTP_BASE_URL_SHASTA ?? '', - TRON_ASSETS_MIGRATION_STAGE: process.env.TRON_ASSETS_MIGRATION_STAGE ?? '', }, polyfills: true, }; diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index e1b9d480..99b6e252 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "HT62C2CzaOh8bzpnWeK/LO9uqW+lcxs+RXnHzaUOXRA=", + "shasum": "8FiFygUypvBGiZUEI1rAWWUTtysoQ7d4SHSbYmpYnnk=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts index bca2223d..f59603ba 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts @@ -369,13 +369,16 @@ export class ClientRequestHandler { const { chainId } = parseCaipAssetType(assetId); const scope = chainId as Network; - const [asset, nativeTokenAsset, bandwidthAsset, energyAsset] = - await this.#assetsService.getAssetsByAccountId(accountId, [ - assetId, - Networks[scope].nativeToken.id, - Networks[scope].bandwidth.id, - Networks[scope].energy.id, - ]); + const assets = await this.#assetsService.getAccountAssetsByIDs(accountId, [ + assetId, + Networks[scope].nativeToken.id, + Networks[scope].bandwidth.id, + Networks[scope].energy.id, + ]); + const asset = assets[assetId]; + const nativeTokenAsset = assets[Networks[scope].nativeToken.id]; + const bandwidthAsset = assets[Networks[scope].bandwidth.id]; + const energyAsset = assets[Networks[scope].energy.id]; const valueBN = new BigNumber(value); const assetToSendBalance = asset ? new BigNumber(asset.uiAmount) : ZERO; @@ -483,7 +486,7 @@ export class ClientRequestHandler { }; } - const asset = await this.#assetsService.getAssetByAccountId( + const asset = await this.#assetsService.getAccountAssetByID( fromAccountId, assetId, ); @@ -523,11 +526,11 @@ export class ClientRequestHandler { }; } - const [[bandwidthAsset, energyAsset], transaction] = await Promise.all([ + const [bandwidthAndEnergyAssets, transaction] = await Promise.all([ /** * Get available Energy and Bandwidth from account assets. */ - this.#assetsService.getAssetsByAccountId(fromAccountId, [ + this.#assetsService.getAccountAssetsByIDs(fromAccountId, [ Networks[scope].bandwidth.id, Networks[scope].energy.id, ]), @@ -545,6 +548,10 @@ export class ClientRequestHandler { }), ]); + const bandwidthAsset = + bandwidthAndEnergyAssets[Networks[scope].bandwidth.id]; + const energyAsset = bandwidthAndEnergyAssets[Networks[scope].energy.id]; + const availableEnergy = energyAsset ? new BigNumber(energyAsset.rawAmount) : ZERO; @@ -672,11 +679,14 @@ export class ClientRequestHandler { /** * Get available Energy and Bandwidth from account assets. */ - const [bandwidthAsset, energyAsset] = - await this.#assetsService.getAssetsByAccountId(accountId, [ + const bandwidthAndEnergyAssets = + await this.#assetsService.getAccountAssetsByIDs(accountId, [ Networks[scope].bandwidth.id, Networks[scope].energy.id, ]); + const bandwidthAsset = + bandwidthAndEnergyAssets[Networks[scope].bandwidth.id]; + const energyAsset = bandwidthAndEnergyAssets[Networks[scope].energy.id]; const availableEnergy = energyAsset ? new BigNumber(energyAsset.rawAmount) @@ -728,7 +738,7 @@ export class ClientRequestHandler { const scope = Network.Mainnet; - const asset = await this.#assetsService.getAssetByAccountId( + const asset = await this.#assetsService.getAccountAssetByID( fromAccountId, Networks[scope].nativeToken.id, ); @@ -758,11 +768,14 @@ export class ClientRequestHandler { /** * Get available Energy and Bandwidth from account assets. */ - const [bandwidthAsset, energyAsset] = - await this.#assetsService.getAssetsByAccountId(fromAccountId, [ + const bandwidthAndEnergyAssets = + await this.#assetsService.getAccountAssetsByIDs(fromAccountId, [ Networks[scope].bandwidth.id, Networks[scope].energy.id, ]); + const bandwidthAsset = + bandwidthAndEnergyAssets[Networks[scope].bandwidth.id]; + const energyAsset = bandwidthAndEnergyAssets[Networks[scope].energy.id]; const availableEnergy = energyAsset ? BigNumber(energyAsset.rawAmount) @@ -804,7 +817,7 @@ export class ClientRequestHandler { const { accountId, assetId, value } = request.params; await this.#accountsService.findByIdOrThrow(accountId); - const asset = await this.#assetsService.getAssetByAccountId( + const asset = await this.#assetsService.getAccountAssetByID( accountId, assetId, ); @@ -851,7 +864,7 @@ export class ClientRequestHandler { const account = await this.#accountsService.findByIdOrThrow(fromAccountId); - const asset = await this.#assetsService.getAssetByAccountId( + const asset = await this.#assetsService.getAccountAssetByID( fromAccountId, assetId, ); @@ -913,7 +926,7 @@ export class ClientRequestHandler { const stakedAssetId = `${assetId}-staked-for-${purpose.toLowerCase()}`; await this.#accountsService.findByIdOrThrow(accountId); - const asset = await this.#assetsService.getAssetByAccountId( + const asset = await this.#assetsService.getAccountAssetByID( accountId, stakedAssetId, ); @@ -967,7 +980,7 @@ export class ClientRequestHandler { const account = await this.#accountsService.findByIdOrThrow(accountId); - const asset = await this.#assetsService.getAssetByAccountId( + const asset = await this.#assetsService.getAccountAssetByID( accountId, stakedAssetId, ); diff --git a/packages/tron-wallet-snap/src/handlers/keyring.ts b/packages/tron-wallet-snap/src/handlers/keyring.ts index 7864a0c6..a2f7085f 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring.ts @@ -196,12 +196,20 @@ export class KeyringHandler implements Keyring { try { validateRequest({ accountId }, ListAccountAssetsStruct); - await this.#getAccountOrThrow(accountId); + const account = await this.#getAccountOrThrow(accountId); this.#logger.info('Listing account assets', { accountId }); - const assetEntities = - await this.#assetsService.getByKeyringAccountId(accountId); + const assetEntities = ( + await Promise.all( + account.scopes.map((scope) => + this.#assetsService.getAccountAssetsByScope( + scope as Network, + accountId, + ), + ), + ) + ).flat(); const result = assetEntities .filter( (asset) => @@ -342,10 +350,18 @@ export class KeyringHandler implements Keyring { this.#logger.info('Getting account balances', { accountId, assets }); - await this.#getAccountOrThrow(accountId); + const account = await this.#getAccountOrThrow(accountId); - const assetsList = - await this.#assetsService.getByKeyringAccountId(accountId); + const assetsList = ( + await Promise.all( + account.scopes.map((scope) => + this.#assetsService.getAccountAssetsByScope( + scope as Network, + accountId, + ), + ), + ) + ).flat(); const assetsToUse = assetsList .filter((asset) => assets.includes(asset.assetType)) diff --git a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts index 24e733d1..7871aa5e 100644 --- a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts +++ b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts @@ -1,137 +1,48 @@ -import { KeyringEvent } from '@metamask/keyring-api'; -import type { - AccountAssetListUpdatedEvent, - AccountBalancesUpdatedEvent, - KeyringAccount, -} from '@metamask/keyring-api'; -import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; import type { AssetConversion, AssetMetadata, FungibleAssetMarketData, - FungibleAssetMetadata, HistoricalPriceIntervals, } from '@metamask/snaps-sdk'; -import { assert } from '@metamask/superstruct'; -import type { CaipAssetType } from '@metamask/utils'; -import { CaipAssetTypeStruct, parseCaipAssetType } from '@metamask/utils'; -import { BigNumber } from 'bignumber.js'; -import { pick } from 'lodash'; +import { + SNAPS_ASSETS_MIGRATION_FLAG_KEYS, + SnapsAssetsMigrationStage, + getSnapsAssetsMigrationNamespace, + parseSnapsAssetsMigrationStage, +} from '@metamask/assets-controller'; +import type { KeyringAccount } from '@metamask/keyring-api'; +import type { CaipAssetType, CaipChainId, Json } from '@metamask/utils'; +import { parseCaipAssetType } from '@metamask/utils'; import type { PriceApiClient } from '../../clients/price-api/PriceApiClient'; -import type { - FiatTicker, - SpotPrice, - SpotPrices, -} from '../../clients/price-api/types'; -import { - GET_HISTORICAL_PRICES_RESPONSE_NULL_OBJECT, - VsCurrencyParamStruct, -} from '../../clients/price-api/types'; import type { SnapClient } from '../../clients/snap/SnapClient'; import type { TokenApiClient } from '../../clients/token-api/TokenApiClient'; -import type { AccountResources } from '../../clients/tron-http'; import type { TronHttpClient } from '../../clients/tron-http/TronHttpClient'; import type { TrongridApiClient } from '../../clients/trongrid/TrongridApiClient'; -import type { - RawTronUnfrozenV2, - Trc20Balance, - TronAccount, -} from '../../clients/trongrid/types'; -import type { KnownCaip19Id, Network } from '../../constants'; -import { - BANDWIDTH_METADATA, - ENERGY_METADATA, - ESSENTIAL_ASSETS, - MAX_BANDWIDTH_METADATA, - MAX_ENERGY_METADATA, - Networks, - TokenMetadata, - TRX_IN_LOCK_PERIOD_METADATA, - TRX_METADATA, - TRX_READY_FOR_WITHDRAWAL_METADATA, - TRX_STAKED_FOR_BANDWIDTH_METADATA, - TRX_STAKED_FOR_ENERGY_METADATA, - TRX_STAKING_REWARDS_METADATA, -} from '../../constants'; -import { configProvider } from '../../context'; +import type { Network } from '../../constants'; import type { AssetEntity } from '../../entities/assets'; -import { toUiAmount } from '../../utils/conversion'; -import { createPrefixedLogger } from '../../utils/logger'; import type { ILogger } from '../../utils/logger'; import type { State, UnencryptedStateValue } from '../state/State'; import type { CoreMessengerCaller } from '../../types/core-messenger'; import type { AssetsRepository } from './AssetsRepository'; -import type { AssetsMigrationMode } from './assetsMigrationMode'; -import { - parseStageFromEnv, - resolveAssetsMigrationMode, -} from './assetsMigrationMode'; -import { mapControllerAsset } from './mapControllerAsset'; +import { CoreAssetsAdapter } from './adapters/CoreAssetsAdapter'; +import { SnapAssetsAdapter } from './adapters/SnapAssetsAdapter'; import { isSnapOwnedAsset } from './snapOwnedAssets'; -import type { - InLockPeriodCaipAssetType, - NativeCaipAssetType, - NftCaipAssetType, - ReadyForWithdrawalCaipAssetType, - ResourceCaipAssetType, - StakedCaipAssetType, - StakingRewardsCaipAssetType, - TokenCaipAssetType, -} from './types'; /** - * Normalized account data structure that provides a consistent shape for both - * active and inactive accounts. This allows extraction functions to work - * without needing to know the account's activation state. + * Assets migration stage used when no remote feature flag is set for the chain. + * Change this value to test Stage 0 / 1 / 2 locally. */ -type NormalizedAccountData = { - /** Native TRX balance in sun (0 for inactive accounts). */ - nativeBalance: number; - /** TRC10 token balances as `{ key: tokenId, value: balance }[]` (empty for inactive accounts). */ - trc10Balances: TronAccount['assetV2']; - /** TRC20 token balances from either account info or fallback endpoint. */ - trc20Balances: Trc20Balance[]; - /** Staking data including frozen balances and delegated resources. */ - stakedData: { - frozenV2: TronAccount['frozenV2']; - unfrozenV2: TronAccount['unfrozenV2']; - accountResource: TronAccount['account_resource'] | undefined; - }; - /** Account resources (energy, bandwidth). Empty object for inactive accounts. */ - resources: AccountResources | Record; - /** Unclaimed staking rewards in sun (0 if no rewards). */ - stakingRewards: number; -}; +const ASSETS_MIGRATION_STAGE = SnapsAssetsMigrationStage.Off; export class AssetsService { - readonly #logger: ILogger; - - readonly #assetsRepository: AssetsRepository; - - readonly #state: State; - - readonly #trongridApiClient: TrongridApiClient; - - readonly #tronHttpClient: TronHttpClient; - - readonly #priceApiClient: PriceApiClient; + readonly #snapAdapter: SnapAssetsAdapter; - readonly #tokenApiClient: TokenApiClient; - - readonly #snapClient: SnapClient; + readonly #coreAdapter: CoreAssetsAdapter; readonly #coreMessenger: CoreMessengerCaller; - readonly #isDev: boolean; - - readonly #envMigrationStage: ReturnType; - - readonly cacheTtlsMilliseconds: { - fiatExchangeRates: number; - spotPrices: number; - historicalPrices: number; - }; + readonly cacheTtlsMilliseconds: SnapAssetsAdapter['cacheTtlsMilliseconds']; constructor({ logger, @@ -154,1673 +65,214 @@ export class AssetsService { snapClient: SnapClient; coreMessenger: CoreMessengerCaller; }) { - this.#logger = createPrefixedLogger(logger, '[🪙 AssetsService]'); - this.#assetsRepository = assetsRepository; - this.#state = state; - this.#trongridApiClient = trongridApiClient; - this.#tronHttpClient = tronHttpClient; - this.#priceApiClient = priceApiClient; - this.#tokenApiClient = tokenApiClient; - this.#snapClient = snapClient; this.#coreMessenger = coreMessenger; - // Static env access so the Snap bundler inlines values at build time. - this.#isDev = process.env.ENVIRONMENT !== 'production'; - this.#envMigrationStage = parseStageFromEnv( - process.env.TRON_ASSETS_MIGRATION_STAGE, - ); - const { cacheTtlsMilliseconds } = configProvider.get().priceApi; - this.cacheTtlsMilliseconds = cacheTtlsMilliseconds; - } - - static isFiat(caipAssetId: CaipAssetType): boolean { - return caipAssetId.includes('swift:0/iso4217:'); - } - - async #resolveMode(chainId: string): Promise { - return resolveAssetsMigrationMode({ - coreMessenger: this.#coreMessenger, - chainId, - isDev: this.#isDev, - envStage: this.#envMigrationStage, + this.#snapAdapter = new SnapAssetsAdapter({ + logger, + assetsRepository, + state, + trongridApiClient, + tronHttpClient, + priceApiClient, + tokenApiClient, + snapClient, + resolveMigrationStage: (chainId) => + this.#resolveMigrationStage(chainId), }); + this.#coreAdapter = new CoreAssetsAdapter({ coreMessenger }); + this.cacheTtlsMilliseconds = this.#snapAdapter.cacheTtlsMilliseconds; } - async getAllAssetsByAccountId(accountId: string): Promise { - return this.#assetsRepository.getByAccountId(accountId); - } - - async getAssetsByAccountId( - accountId: string, - assetTypes: string[], - ): Promise<(AssetEntity | null)[]> { - return Promise.all( - assetTypes.map(async (assetType) => - this.getAssetByAccountId(accountId, assetType), - ), + async #resolveMigrationStage( + chainId: string, + ): Promise { + const { remoteFeatureFlags } = await this.#coreMessenger.call( + 'RemoteFeatureFlagController:getState', ); - } - async getAssetByAccountId( - accountId: string, - assetType: string, - ): Promise { - if (isSnapOwnedAsset(assetType)) { - return this.#assetsRepository.getByAccountIdAndAssetType( - accountId, - assetType, - ); - } + const namespace = getSnapsAssetsMigrationNamespace(chainId as CaipChainId); - const { chainId } = parseCaipAssetType(assetType as CaipAssetType); - const mode = await this.#resolveMode(chainId); + if (namespace) { + const flagKey = SNAPS_ASSETS_MIGRATION_FLAG_KEYS[namespace]; - if (mode === 'controller') { - const result = await this.#coreMessenger.call( - 'AssetsController:getAsset', - accountId, - assetType, - ); + if (flagKey in remoteFeatureFlags) { + const remoteStage = parseSnapsAssetsMigrationStage( + remoteFeatureFlags[flagKey] as Json | undefined, + ); - if (!result) { - return null; + if (remoteStage !== undefined) { + return remoteStage; + } } - - return mapControllerAsset(accountId, assetType, result); } - return this.#assetsRepository.getByAccountIdAndAssetType( - accountId, - assetType, - ); + return ASSETS_MIGRATION_STAGE; } - /** - * Fetches all assets and balances for an account. - * - * Data Sources: - * - `getAccountInfoByAddress`: TRX balance, TRC10 tokens, TRC20 tokens (active accounts only) - * - `getAccountResources`: Energy and Bandwidth (returns {} for inactive accounts) - * - `getTrc20BalancesByAddress`: TRC20 balances fallback (works for inactive accounts) - * - * Logic Flow: - * 1. Fetch account info, resources, and TRC20 fallback (for inactive accounts) - * 2. Normalize data into consistent shape via `#buildAccountData` - * 3. Extract all assets via `#extractAssets` - * 4. Fetch metadata and prices in parallel - * 5. Enrich assets with metadata via `#enrichAssetsWithMetadata` - * 6. Filter spam tokens via `#filterTokensWithoutPriceData` - * - * @param scope - The network to query. - * @param account - The keyring account. - * @returns Promise - Array of assets with balances. - */ - async fetchAssetsAndBalancesForAccount( - scope: Network, - account: KeyringAccount, - ): Promise { - this.#logger.info('Fetching assets and balances by account', { - account, - scope, - }); - - const mode = await this.#resolveMode(scope); - - const [ - tronAccountInfoRequest, - tronAccountResourcesRequest, - stakingRewardsRequest, - ] = await Promise.allSettled([ - this.#trongridApiClient.getAccountInfoByAddress(scope, account.address), - this.#tronHttpClient.getAccountResources(scope, account.address), - this.#tronHttpClient.getReward(scope, account.address), - ]); - - const isInactiveAccount = tronAccountInfoRequest.status === 'rejected'; - if (isInactiveAccount) { - this.#logger.info( - 'Account info request failed, treating as inactive account', - { account, scope }, - ); + async getAccountAssetByID( + accountId: string, + assetId: string, + ): Promise { + if (isSnapOwnedAsset(assetId)) { + return this.#snapAdapter.getAccountAssetByID(accountId, assetId); } - const trc20BalancesFallback = - mode === 'snap' && isInactiveAccount - ? await this.#trongridApiClient - .getTrc20BalancesByAddress(scope, account.address) - .catch(async (error) => { - await this.#snapClient.trackError(error as Error); - this.#logger.warn( - 'Failed to fetch TRC20 balances for inactive account', - { error, account, scope }, - ); - return []; - }) - : []; + const { chainId } = parseCaipAssetType(assetId as CaipAssetType); + const stage = await this.#resolveMigrationStage(chainId); - const accountData = this.#buildAccountData({ - tronAccountInfoRequest, - tronAccountResourcesRequest, - trc20BalancesFallback, - stakingRewardsRequest, - }); - - const rawAssets = - mode === 'controller' - ? this.#extractSnapOwnedAssets(account, scope, accountData) - : this.#extractAssets(account, scope, accountData); - - const assetTypes = rawAssets.map((asset) => asset.assetType); - const priceableAssetTypes = this.#getPriceableAssetTypes(rawAssets); - - const [assetsMetadata, spotPrices] = await Promise.all([ - this.getAssetsMetadata(assetTypes), - this.#priceApiClient - .getMultipleSpotPrices(priceableAssetTypes, 'usd') - .catch(async (error) => { - await this.#snapClient.trackError(error as Error); - return {}; - }), - ]); - - const enrichedAssets = this.#enrichAssetsWithMetadata( - rawAssets, - assetsMetadata, - ); - return this.#filterTokensWithoutPriceData(enrichedAssets, spotPrices); - } + if (stage === SnapsAssetsMigrationStage.Off) { + return this.#snapAdapter.getAccountAssetByID(accountId, assetId); + } - /** - * Filters out spam tokens (those without price data). - * Essential assets are always kept. Tokens need price data to be included. - * - * @param assets - The assets to filter. - * @param spotPrices - Pre-fetched USD prices for assets. - * @returns The filtered assets. - */ - #filterTokensWithoutPriceData( - assets: AssetEntity[], - spotPrices: SpotPrices | Record, - ): AssetEntity[] { - const filtered = assets.filter((asset) => { - // Essential assets (TRX, staked, energy, bandwidth) are always kept - if (ESSENTIAL_ASSETS.includes(asset.assetType)) { - return true; + if (stage === SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback) { + try { + return await this.#coreAdapter.getAccountAssetByID(accountId, assetId); + } catch { + return this.#snapAdapter.getAccountAssetByID(accountId, assetId); } - // Tokens: keep only if they have price data - const spotPrice = (spotPrices as SpotPrices)[asset.assetType]; - return typeof spotPrice?.price === 'number'; - }); - - return filtered; - } - - /** - * Normalizes raw API responses into a consistent shape for both active and inactive accounts. - * This allows extraction functions to work without needing to know the account's activation state. - * - * @param params - The raw API responses to normalize. - * @param params.tronAccountInfoRequest - The settled promise result from getAccountInfoByAddress. - * @param params.tronAccountResourcesRequest - The settled promise result from getAccountResources. - * @param params.trc20BalancesFallback - TRC20 balances from fallback endpoint (empty for active accounts). - * @param params.stakingRewardsRequest - The settled promise result from getReward. - * @returns NormalizedAccountData - Consistent data shape for extraction. - */ - #buildAccountData({ - tronAccountInfoRequest, - tronAccountResourcesRequest, - trc20BalancesFallback, - stakingRewardsRequest, - }: { - tronAccountInfoRequest: PromiseSettledResult; - tronAccountResourcesRequest: PromiseSettledResult; - trc20BalancesFallback: Trc20Balance[]; - stakingRewardsRequest: PromiseSettledResult; - }): NormalizedAccountData { - const isInactiveAccount = tronAccountInfoRequest.status === 'rejected'; - const resources = - tronAccountResourcesRequest.status === 'fulfilled' - ? tronAccountResourcesRequest.value - : {}; - const stakingRewards = - stakingRewardsRequest.status === 'fulfilled' - ? Math.max(0, stakingRewardsRequest.value) - : 0; - - if (isInactiveAccount) { - return { - nativeBalance: 0, - trc10Balances: [], - trc20Balances: trc20BalancesFallback, - stakedData: { - frozenV2: [], - unfrozenV2: [], - accountResource: undefined, - }, - resources, - stakingRewards, - }; } - const tronAccountInfo = tronAccountInfoRequest.value; - return { - nativeBalance: tronAccountInfo.balance ?? 0, - trc10Balances: tronAccountInfo.assetV2 ?? [], - trc20Balances: tronAccountInfo.trc20 ?? [], - stakedData: { - frozenV2: tronAccountInfo.frozenV2 ?? [], - unfrozenV2: tronAccountInfo.unfrozenV2 ?? [], - accountResource: tronAccountInfo.account_resource, - }, - resources, - stakingRewards, - }; - } - - /** - * Extracts all assets from normalized account data. - * Coordinates calls to individual extraction functions. - * - * @param account - The keyring account. - * @param scope - The network. - * @param data - Normalized account data. - * @returns AssetEntity[] - Array of all extracted assets. - */ - #extractAssets( - account: KeyringAccount, - scope: Network, - data: NormalizedAccountData, - ): AssetEntity[] { - return [ - this.#extractNativeAsset(account, scope, data.nativeBalance), - ...this.#extractSnapOwnedAssets(account, scope, data), - ...this.#extractTrc10Assets(account, scope, data.trc10Balances), - ...this.#extractTrc20Assets(account, scope, data.trc20Balances), - ]; + return this.#coreAdapter.getAccountAssetByID(accountId, assetId); } - #extractSnapOwnedAssets( - account: KeyringAccount, - scope: Network, - data: NormalizedAccountData, - ): AssetEntity[] { - return [ - ...this.#extractStakedNativeAssets(account, scope, data.stakedData), - this.#extractReadyForWithdrawalAsset(account, scope, data.stakedData), - this.#extractInLockPeriodAsset(account, scope, data.stakedData), - this.#extractStakingRewardsAsset(account, scope, data.stakingRewards), - ...this.#extractBandwidth({ - account, - scope, - tronAccountResources: data.resources, - }), - ...this.#extractEnergy({ - account, - scope, - tronAccountResources: data.resources, - }), - ]; - } - - /** - * Returns the asset types that can be priced (native, TRC10, TRC20). - * Staked, energy, and bandwidth assets have non-compliant CAIP IDs that would fail the Price API. - * - * @param assets - Array of assets to filter. - * @returns CaipAssetType[] - Array of priceable asset types. - */ - #getPriceableAssetTypes(assets: AssetEntity[]): CaipAssetType[] { - return assets - .filter( - (asset) => - asset.assetType.includes('/slip44:') || - asset.assetType.includes('/trc10:') || - asset.assetType.includes('/trc20:'), - ) - .map((asset) => asset.assetType); - } - - /** - * Enriches assets with metadata (symbol, decimals, iconUrl) and calculates uiAmount. - * - * @param assets - Raw assets to enrich. - * @param assetsMetadata - Metadata lookup by asset type. - * @returns AssetEntity[] - Enriched assets. - */ - #enrichAssetsWithMetadata( - assets: AssetEntity[], - assetsMetadata: Record, - ): AssetEntity[] { - return assets.map((asset) => { - const metadata = assetsMetadata[ - asset.assetType - ] as FungibleAssetMetadata | null; + async getAccountAssetsByIDs( + accountId: string, + assetIds: string[], + ): Promise> { + if (assetIds.length === 0) { + return {}; + } - const { - symbol: initialSymbol, - decimals: initialDecimals = 0, - iconUrl: initialIconUrl, - } = asset; - let symbol = initialSymbol; - let decimals = initialDecimals; - let iconUrl = initialIconUrl; + const result: Record = {}; + const fungibleIds: string[] = []; - if (metadata?.fungible) { - const unit = metadata.units?.[0]; - if (unit) { - symbol = unit.symbol ?? metadata.symbol ?? symbol; - decimals = unit.decimals ?? decimals; - } else { - symbol = metadata?.symbol ?? symbol; - } - iconUrl = metadata.iconUrl ?? iconUrl; + for (const assetId of assetIds) { + if (isSnapOwnedAsset(assetId)) { + result[assetId] = await this.#snapAdapter.getAccountAssetByID( + accountId, + assetId, + ); + } else { + fungibleIds.push(assetId); } + } - const uiAmount = toUiAmount(asset.rawAmount, decimals).toString(); + if (fungibleIds.length === 0) { + return result; + } + const { chainId } = parseCaipAssetType(fungibleIds[0] as CaipAssetType); + const stage = await this.#resolveMigrationStage(chainId); + + if (stage === SnapsAssetsMigrationStage.Off) { return { - ...asset, - symbol, - decimals, - uiAmount, - iconUrl, + ...result, + ...(await this.#snapAdapter.getAccountAssetsByIDs(accountId, fungibleIds)), }; - }); - } - - /** - * Extracts the native TRX asset from the balance. - * - * @param account - The keyring account. - * @param scope - The network. - * @param balance - The native balance in sun. - * @returns AssetEntity - The native TRX asset. - */ - #extractNativeAsset( - account: KeyringAccount, - scope: Network, - balance: number, - ): AssetEntity { - return { - assetType: Networks[scope].nativeToken.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].nativeToken.symbol, - decimals: Networks[scope].nativeToken.decimals, - rawAmount: balance.toString(), - uiAmount: toUiAmount( - balance, - Networks[scope].nativeToken.decimals, - ).toString(), - iconUrl: Networks[scope].nativeToken.iconUrl, - }; - } - - /** - * Extracts staked TRX assets (for bandwidth and energy). - * - * @param account - The keyring account. - * @param scope - The network. - * @param stakedData - Staking data including frozen balances and delegated resources. - * @returns AssetEntity[] - Array of staked assets (always 2: bandwidth and energy, amounts may be 0). - */ - #extractStakedNativeAssets( - account: KeyringAccount, - scope: Network, - stakedData: NormalizedAccountData['stakedData'], - ): AssetEntity[] { - const assets: AssetEntity[] = []; - - let stakedBandwidthAmount = 0; - let stakedEnergyAmount = 0; - - stakedData.frozenV2?.forEach((frozen) => { - const amount = frozen.amount ?? 0; - - if (frozen.type === 'ENERGY') { - stakedEnergyAmount += amount; - } else if (!frozen.type) { - // Item without type is for bandwidth - stakedBandwidthAmount += amount; - } - }); - - const delegatedBandwidth = - stakedData.accountResource?.delegated_frozenV2_balance_for_bandwidth ?? 0; - const delegatedEnergy = - stakedData.accountResource?.delegated_frozenV2_balance_for_energy ?? 0; - - stakedBandwidthAmount += delegatedBandwidth; - stakedEnergyAmount += delegatedEnergy; - - assets.push({ - assetType: Networks[scope].stakedForBandwidth.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].stakedForBandwidth.symbol, - decimals: Networks[scope].stakedForBandwidth.decimals, - rawAmount: stakedBandwidthAmount.toString(), - uiAmount: toUiAmount( - stakedBandwidthAmount, - Networks[scope].stakedForBandwidth.decimals, - ).toString(), - iconUrl: Networks[scope].stakedForBandwidth.iconUrl, - }); - - assets.push({ - assetType: Networks[scope].stakedForEnergy.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].stakedForEnergy.symbol, - decimals: Networks[scope].stakedForEnergy.decimals, - rawAmount: stakedEnergyAmount.toString(), - uiAmount: toUiAmount( - stakedEnergyAmount, - Networks[scope].stakedForEnergy.decimals, - ).toString(), - iconUrl: Networks[scope].stakedForEnergy.iconUrl, - }); - - return assets; - } - - /** - * Extracts TRX ready for withdrawal (unstaked TRX that has completed the withdrawal period). - * - * @param account - The keyring account. - * @param scope - The network. - * @param stakedData - Staking data including unfrozen balances. - * @returns AssetEntity - The ready-for-withdrawal asset (amount may be 0). - */ - #extractReadyForWithdrawalAsset( - account: KeyringAccount, - scope: Network, - stakedData: NormalizedAccountData['stakedData'], - ): AssetEntity { - const currentTimestamp = Date.now(); - let readyForWithdrawalAmount = 0; - - stakedData.unfrozenV2?.forEach((unfrozen: RawTronUnfrozenV2) => { - const expireTime = unfrozen.unfreeze_expire_time ?? 0; - const amount = unfrozen.unfreeze_amount ?? 0; - - if (expireTime <= currentTimestamp && amount > 0) { - readyForWithdrawalAmount += amount; - } - }); - - const { id, symbol, decimals, iconUrl } = - Networks[scope].readyForWithdrawal; - - return { - assetType: id, - keyringAccountId: account.id, - network: scope, - symbol, - decimals, - rawAmount: readyForWithdrawalAmount.toString(), - uiAmount: toUiAmount(readyForWithdrawalAmount, decimals).toString(), - iconUrl, - }; - } - - /** - * Extracts staking rewards asset (unclaimed voting rewards). - * - * @param account - The keyring account. - * @param scope - The network. - * @param stakingRewards - Unclaimed staking rewards in sun. - * @returns AssetEntity - The staking rewards asset. - */ - #extractStakingRewardsAsset( - account: KeyringAccount, - scope: Network, - stakingRewards: number, - ): AssetEntity { - return { - assetType: Networks[scope].stakingRewards.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].stakingRewards.symbol, - decimals: Networks[scope].stakingRewards.decimals, - rawAmount: stakingRewards.toString(), - uiAmount: toUiAmount( - stakingRewards, - Networks[scope].stakingRewards.decimals, - ).toString(), - iconUrl: Networks[scope].stakingRewards.iconUrl, - }; - } - - /** - * Extracts TRX that is in the lock period (unstaked but lock period not yet ended). - * This represents TRX that the user has initiated unstaking for but must wait - * the 14-day lock period before they can withdraw. - * - * @param account - The keyring account. - * @param scope - The network. - * @param stakedData - Staking data including unfrozen balances. - * @returns AssetEntity - The in-lock-period asset (amount may be 0). - */ - #extractInLockPeriodAsset( - account: KeyringAccount, - scope: Network, - stakedData: NormalizedAccountData['stakedData'], - ): AssetEntity { - const currentTimestamp = Date.now(); - let inLockPeriodAmount = 0; - - stakedData.unfrozenV2?.forEach((unfrozen: RawTronUnfrozenV2) => { - const expireTime = unfrozen.unfreeze_expire_time ?? 0; - const amount = unfrozen.unfreeze_amount ?? 0; - - if (expireTime > currentTimestamp && amount > 0) { - inLockPeriodAmount += amount; - } - }); - - const { id, symbol, decimals, iconUrl } = Networks[scope].inLockPeriod; - - return { - assetType: id, - keyringAccountId: account.id, - network: scope, - symbol, - decimals, - rawAmount: inLockPeriodAmount.toString(), - uiAmount: toUiAmount(inLockPeriodAmount, decimals).toString(), - iconUrl, - }; - } - - /** - * Extracts current and maximum bandwidth from the account resources. - * - * @param options - Options object. - * @param options.account - The account to extract bandwidth for. - * @param options.scope - The network to extract bandwidth for. - * @param options.tronAccountResources - The account resources to extract bandwidth for. - * @returns The bandwidth assets. - */ - #extractBandwidth({ - account, - scope, - tronAccountResources, - }: { - account: KeyringAccount; - scope: Network; - tronAccountResources: AccountResources | Record; - }): AssetEntity[] { - const freeBandwidth = tronAccountResources?.freeNetLimit ?? 0; - const stakingBandwidth = tronAccountResources?.NetLimit ?? 0; - const maximumBandwidth = freeBandwidth + stakingBandwidth; - - const usedFreeBandwidth = tronAccountResources?.freeNetUsed ?? 0; - const usedStakingBandwidth = tronAccountResources?.NetUsed ?? 0; - const usedBandwidth = usedFreeBandwidth + usedStakingBandwidth; - - const availableBandwidth = Math.max(0, maximumBandwidth - usedBandwidth); - - return [ - { - assetType: Networks[scope].bandwidth.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].bandwidth.symbol, - decimals: Networks[scope].bandwidth.decimals, - rawAmount: availableBandwidth.toString(), - uiAmount: availableBandwidth.toString(), - iconUrl: Networks[scope].bandwidth.iconUrl, - }, - { - assetType: Networks[scope].maximumBandwidth.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].maximumBandwidth.symbol, - decimals: Networks[scope].maximumBandwidth.decimals, - rawAmount: maximumBandwidth.toString(), - uiAmount: maximumBandwidth.toString(), - iconUrl: Networks[scope].maximumBandwidth.iconUrl, - }, - ]; - } - - /** - * Extracts current and maximum energy from the account resources. - * - * @param options - Options object. - * @param options.account - The keyring account. - * @param options.scope - The network. - * @param options.tronAccountResources - Account resources (energy, bandwidth). - * @returns AssetEntity[] - Array containing energy and maximum energy assets. - */ - #extractEnergy({ - account, - scope, - tronAccountResources, - }: { - account: KeyringAccount; - scope: Network; - tronAccountResources: AccountResources | Record; - }): AssetEntity[] { - const maximumEnergy = tronAccountResources?.EnergyLimit ?? 0; - const usedEnergy = tronAccountResources?.EnergyUsed ?? 0; - - /** - * We might have used more Energy than the maximum allocated - */ - const availableEnergy = Math.max(0, maximumEnergy - usedEnergy); - - return [ - { - assetType: Networks[scope].energy.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].energy.symbol, - decimals: Networks[scope].energy.decimals, - rawAmount: availableEnergy.toString(), - uiAmount: availableEnergy.toString(), - iconUrl: Networks[scope].energy.iconUrl, - }, - { - assetType: Networks[scope].maximumEnergy.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].maximumEnergy.symbol, - decimals: Networks[scope].maximumEnergy.decimals, - rawAmount: maximumEnergy.toString(), - uiAmount: maximumEnergy.toString(), - iconUrl: Networks[scope].maximumEnergy.iconUrl, - }, - ]; - } + } - /** - * Extracts TRC10 assets from the balances array. - * - * @param account - The keyring account. - * @param scope - The network. - * @param trc10Balances - TRC10 token balances as `{ key: tokenId, value: balance }[]`. - * @returns AssetEntity[] - Array of TRC10 asset entities. - */ - #extractTrc10Assets( - account: KeyringAccount, - scope: Network, - trc10Balances: TronAccount['assetV2'], - ): AssetEntity[] { - return ( - trc10Balances?.flatMap((tokenObject) => { - // assetV2 has structure: { "key": "token_id", "value": "balance" } + if (stage === SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback) { + try { return { - assetType: `${scope}/trc10:${tokenObject.key}` as TokenCaipAssetType, - keyringAccountId: account.id, - network: scope, - symbol: '', - decimals: 0, - rawAmount: tokenObject.value?.toString() ?? '0', - uiAmount: '0', - iconUrl: '', // Will be enriched with metadata later + ...result, + ...(await this.#coreAdapter.getAccountAssetsByIDs( + accountId, + fungibleIds, + )), }; - }) ?? [] - ); - } - - /** - * Extracts TRC20 assets from a balances array. - * Works with both active accounts (tronAccountInfo.trc20) and inactive accounts (getTrc20BalancesByAddress). - * - * @param account - The keyring account. - * @param scope - The network. - * @param trc20Balances - Array of `Record` objects (e.g., `[{ "TContractAddr": "1000" }]`). - * @returns AssetEntity[] - Array of TRC20 asset entities. - */ - #extractTrc20Assets( - account: KeyringAccount, - scope: Network, - trc20Balances: Trc20Balance[], - ): AssetEntity[] { - return trc20Balances.flatMap((tokenObject) => { - return Object.entries(tokenObject).map(([address, balance]) => { + } catch { return { - assetType: `${scope}/trc20:${address}` as TokenCaipAssetType, - keyringAccountId: account.id, - network: scope, - symbol: '', - decimals: 0, - rawAmount: balance, - uiAmount: '0', - iconUrl: '', // Will be enriched with metadata later - }; - }); - }); - } - - async getAssetsMetadata( - assetTypes: CaipAssetType[], - ): Promise> { - this.#logger.info('Fetching metadata for assets', assetTypes); - - const { - nativeAssetTypes, - stakedNativeAssetTypes, - readyForWithdrawalAssetTypes, - inLockPeriodAssetTypes, - stakingRewardsAssetTypes, - energyAssetTypes, - maximunEnergyAssetTypes, - bandwidthAssetTypes, - maximunBandwidthAssetTypes, - tokenTrc10AssetTypes, - tokenTrc20AssetTypes, - } = this.#splitAssetsByType(assetTypes); - - const nativeTokensMetadata = - this.#getNativeTokensMetadata(nativeAssetTypes); - const stakedTokensMetadata = this.#getStakedTokensMetadata( - stakedNativeAssetTypes, - ); - const readyForWithdrawalTokensMetadata = - this.#getReadyForWithdrawalTokensMetadata(readyForWithdrawalAssetTypes); - const inLockPeriodTokensMetadata = this.#getInLockPeriodMetadata( - inLockPeriodAssetTypes, - ); - const stakingRewardsMetadata = this.#getStakingRewardsMetadata( - stakingRewardsAssetTypes, - ); - const energyTokensMetadata = this.#getEnergyMetadata(energyAssetTypes); - const maximunEnergyTokensMetadata = this.#getMaximunEnergyMetadata( - maximunEnergyAssetTypes, - ); - const bandwidthTokensMetadata = - this.#getBandwidthMetadata(bandwidthAssetTypes); - const maximunBandwidthTokensMetadata = this.#getMaximunBandwidthMetadata( - maximunBandwidthAssetTypes, - ); - const tokensMetadata = await this.#getTokensMetadata([ - ...tokenTrc10AssetTypes, - ...tokenTrc20AssetTypes, - ]); - - const result = { - ...nativeTokensMetadata, - ...stakedTokensMetadata, - ...readyForWithdrawalTokensMetadata, - ...inLockPeriodTokensMetadata, - ...stakingRewardsMetadata, - ...energyTokensMetadata, - ...maximunEnergyTokensMetadata, - ...bandwidthTokensMetadata, - ...maximunBandwidthTokensMetadata, - ...tokensMetadata, - }; - - this.#logger.info('Resolved assets metadata', { assetTypes, result }); - - return result; - } - - #splitAssetsByType(assetTypes: CaipAssetType[]): { - nativeAssetTypes: NativeCaipAssetType[]; - stakedNativeAssetTypes: StakedCaipAssetType[]; - readyForWithdrawalAssetTypes: ReadyForWithdrawalCaipAssetType[]; - inLockPeriodAssetTypes: InLockPeriodCaipAssetType[]; - stakingRewardsAssetTypes: StakingRewardsCaipAssetType[]; - energyAssetTypes: ResourceCaipAssetType[]; - maximunEnergyAssetTypes: ResourceCaipAssetType[]; - bandwidthAssetTypes: ResourceCaipAssetType[]; - maximunBandwidthAssetTypes: ResourceCaipAssetType[]; - tokenTrc10AssetTypes: TokenCaipAssetType[]; - tokenTrc20AssetTypes: TokenCaipAssetType[]; - nftAssetTypes: NftCaipAssetType[]; - } { - const nativeAssetTypes = assetTypes.filter((assetType) => - assetType.endsWith('/slip44:195'), - ) as NativeCaipAssetType[]; - const stakedNativeAssetTypes = assetTypes.filter((assetType) => - assetType.includes('/slip44:195-staked-for-'), - ) as StakedCaipAssetType[]; - const readyForWithdrawalAssetTypes = assetTypes.filter((assetType) => - assetType.endsWith('/slip44:195-ready-for-withdrawal'), - ) as ReadyForWithdrawalCaipAssetType[]; - const inLockPeriodAssetTypes = assetTypes.filter((assetType) => - assetType.endsWith('/slip44:195-in-lock-period'), - ) as InLockPeriodCaipAssetType[]; - const stakingRewardsAssetTypes = assetTypes.filter((assetType) => - assetType.endsWith('/slip44:195-staking-rewards'), - ) as StakingRewardsCaipAssetType[]; - const energyAssetTypes = assetTypes.filter((assetType) => - assetType.endsWith('/slip44:energy'), - ) as ResourceCaipAssetType[]; - const maximunEnergyAssetTypes = assetTypes.filter((assetType) => - assetType.endsWith('/slip44:maximum-energy'), - ) as ResourceCaipAssetType[]; - const bandwidthAssetTypes = assetTypes.filter((assetType) => - assetType.endsWith('/slip44:bandwidth'), - ) as ResourceCaipAssetType[]; - const maximunBandwidthAssetTypes = assetTypes.filter((assetType) => - assetType.endsWith('/slip44:maximum-bandwidth'), - ) as ResourceCaipAssetType[]; - const tokenTrc10AssetTypes = assetTypes.filter((assetType) => - assetType.includes('/trc10:'), - ) as TokenCaipAssetType[]; - const tokenTrc20AssetTypes = assetTypes.filter((assetType) => - assetType.includes('/trc20:'), - ) as TokenCaipAssetType[]; - const nftAssetTypes = assetTypes.filter((assetType) => - assetType.includes('/trc721:'), - ) as NftCaipAssetType[]; - - return { - nativeAssetTypes, - stakedNativeAssetTypes, - readyForWithdrawalAssetTypes, - inLockPeriodAssetTypes, - stakingRewardsAssetTypes, - energyAssetTypes, - maximunEnergyAssetTypes, - bandwidthAssetTypes, - maximunBandwidthAssetTypes, - tokenTrc10AssetTypes, - tokenTrc20AssetTypes, - nftAssetTypes, - }; - } - - #getNativeTokensMetadata( - assetTypes: NativeCaipAssetType[], - ): Record { - const nativeTokensMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - nativeTokensMetadata[assetType] = { - fungible: TRX_METADATA.fungible, - name: TRX_METADATA.name, - symbol: TRX_METADATA.symbol, - iconUrl: TRX_METADATA.iconUrl, - units: [ - { - decimals: TRX_METADATA.decimals, - symbol: TRX_METADATA.symbol, - name: TRX_METADATA.name, - }, - ], - }; - } - - return nativeTokensMetadata; - } - - #getStakedTokensMetadata( - assetTypes: StakedCaipAssetType[], - ): Record { - // Can either be Staked for Bandwidth or Staked for Energy - const stakedTokensMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - const isForBandwdidth = assetType.endsWith('staked-for-bandwidth'); - - if (isForBandwdidth) { - stakedTokensMetadata[assetType] = { - fungible: TRX_STAKED_FOR_BANDWIDTH_METADATA.fungible, - name: TRX_STAKED_FOR_BANDWIDTH_METADATA.name, - symbol: TRX_STAKED_FOR_BANDWIDTH_METADATA.symbol, - iconUrl: TRX_STAKED_FOR_BANDWIDTH_METADATA.iconUrl, - units: [ - { - decimals: TRX_STAKED_FOR_BANDWIDTH_METADATA.decimals, - symbol: TRX_STAKED_FOR_BANDWIDTH_METADATA.symbol, - name: TRX_STAKED_FOR_BANDWIDTH_METADATA.name, - }, - ], - }; - } - - const isForEnergy = assetType.endsWith('staked-for-energy'); - - if (isForEnergy) { - stakedTokensMetadata[assetType] = { - fungible: TRX_STAKED_FOR_ENERGY_METADATA.fungible, - name: TRX_STAKED_FOR_ENERGY_METADATA.name, - symbol: TRX_STAKED_FOR_ENERGY_METADATA.symbol, - iconUrl: TRX_STAKED_FOR_ENERGY_METADATA.iconUrl, - units: [ - { - decimals: TRX_STAKED_FOR_ENERGY_METADATA.decimals, - symbol: TRX_STAKED_FOR_ENERGY_METADATA.symbol, - name: TRX_STAKED_FOR_ENERGY_METADATA.name, - }, - ], + ...result, + ...(await this.#snapAdapter.getAccountAssetsByIDs( + accountId, + fungibleIds, + )), }; } } - return stakedTokensMetadata; - } - - #getReadyForWithdrawalTokensMetadata( - assetTypes: ReadyForWithdrawalCaipAssetType[], - ): Record { - const readyForWithdrawalTokensMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - readyForWithdrawalTokensMetadata[assetType] = { - fungible: TRX_READY_FOR_WITHDRAWAL_METADATA.fungible, - name: TRX_READY_FOR_WITHDRAWAL_METADATA.name, - symbol: TRX_READY_FOR_WITHDRAWAL_METADATA.symbol, - iconUrl: TRX_READY_FOR_WITHDRAWAL_METADATA.iconUrl, - units: [ - { - decimals: TRX_READY_FOR_WITHDRAWAL_METADATA.decimals, - symbol: TRX_READY_FOR_WITHDRAWAL_METADATA.symbol, - name: TRX_READY_FOR_WITHDRAWAL_METADATA.name, - }, - ], - }; - } - - return readyForWithdrawalTokensMetadata; - } - - #getStakingRewardsMetadata( - assetTypes: StakingRewardsCaipAssetType[], - ): Record { - const stakingRewardsMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - stakingRewardsMetadata[assetType] = { - fungible: TRX_STAKING_REWARDS_METADATA.fungible, - name: TRX_STAKING_REWARDS_METADATA.name, - symbol: TRX_STAKING_REWARDS_METADATA.symbol, - iconUrl: TRX_STAKING_REWARDS_METADATA.iconUrl, - units: [ - { - decimals: TRX_STAKING_REWARDS_METADATA.decimals, - symbol: TRX_STAKING_REWARDS_METADATA.symbol, - name: TRX_STAKING_REWARDS_METADATA.name, - }, - ], - }; - } - - return stakingRewardsMetadata; - } - - #getInLockPeriodMetadata( - assetTypes: InLockPeriodCaipAssetType[], - ): Record { - const inLockPeriodTokensMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - inLockPeriodTokensMetadata[assetType] = { - fungible: TRX_IN_LOCK_PERIOD_METADATA.fungible, - name: TRX_IN_LOCK_PERIOD_METADATA.name, - symbol: TRX_IN_LOCK_PERIOD_METADATA.symbol, - iconUrl: TRX_IN_LOCK_PERIOD_METADATA.iconUrl, - units: [ - { - decimals: TRX_IN_LOCK_PERIOD_METADATA.decimals, - symbol: TRX_IN_LOCK_PERIOD_METADATA.symbol, - name: TRX_IN_LOCK_PERIOD_METADATA.name, - }, - ], - }; - } - - return inLockPeriodTokensMetadata; - } - - #getBandwidthMetadata( - assetTypes: ResourceCaipAssetType[], - ): Record { - const bandwidthTokensMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - bandwidthTokensMetadata[assetType] = { - fungible: BANDWIDTH_METADATA.fungible, - name: BANDWIDTH_METADATA.name, - symbol: BANDWIDTH_METADATA.symbol, - iconUrl: BANDWIDTH_METADATA.iconUrl, - units: [ - { - decimals: BANDWIDTH_METADATA.decimals, - symbol: BANDWIDTH_METADATA.symbol, - name: BANDWIDTH_METADATA.name, - }, - ], - }; - } - - return bandwidthTokensMetadata; - } - - #getMaximunBandwidthMetadata( - assetTypes: ResourceCaipAssetType[], - ): Record { - const maximunBandwidthTokensMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - maximunBandwidthTokensMetadata[assetType] = { - fungible: MAX_BANDWIDTH_METADATA.fungible, - name: MAX_BANDWIDTH_METADATA.name, - symbol: MAX_BANDWIDTH_METADATA.symbol, - iconUrl: MAX_BANDWIDTH_METADATA.iconUrl, - units: [ - { - decimals: MAX_BANDWIDTH_METADATA.decimals, - symbol: MAX_BANDWIDTH_METADATA.symbol, - name: MAX_BANDWIDTH_METADATA.name, - }, - ], - }; - } - - return maximunBandwidthTokensMetadata; - } - - #getEnergyMetadata( - assetTypes: ResourceCaipAssetType[], - ): Record { - const energyTokensMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - energyTokensMetadata[assetType] = { - fungible: ENERGY_METADATA.fungible, - name: ENERGY_METADATA.name, - symbol: ENERGY_METADATA.symbol, - iconUrl: ENERGY_METADATA.iconUrl, - units: [ - { - decimals: ENERGY_METADATA.decimals, - symbol: ENERGY_METADATA.symbol, - name: ENERGY_METADATA.name, - }, - ], - }; - } - - return energyTokensMetadata; - } - - #getMaximunEnergyMetadata( - assetTypes: ResourceCaipAssetType[], - ): Record { - const maximunEnergyTokensMetadata: Record< - CaipAssetType, - FungibleAssetMetadata | null - > = {}; - - for (const assetType of assetTypes) { - maximunEnergyTokensMetadata[assetType] = { - fungible: MAX_ENERGY_METADATA.fungible, - name: MAX_ENERGY_METADATA.name, - symbol: MAX_ENERGY_METADATA.symbol, - iconUrl: MAX_ENERGY_METADATA.iconUrl, - units: [ - { - decimals: MAX_ENERGY_METADATA.decimals, - symbol: MAX_ENERGY_METADATA.symbol, - name: MAX_ENERGY_METADATA.name, - }, - ], - }; - } - - return maximunEnergyTokensMetadata; - } - - async #getTokensMetadata( - assetTypes: TokenCaipAssetType[], - ): Promise> { - return this.#tokenApiClient.getTokensMetadata(assetTypes); - } - - /** - * Checks if the asset has changed compared to passed assets lookup. - * - * @param asset - The asset to check. - * @param assetsLookup - The lookup table to check against. - * @returns True if the asset has changed, false otherwise. - */ - static hasChanged(asset: AssetEntity, assetsLookup: AssetEntity[]): boolean { - const savedAsset = assetsLookup.find( - (item) => - item.keyringAccountId === asset.keyringAccountId && - item.assetType === asset.assetType, - ); - - if (!savedAsset) { - return true; - } - - return savedAsset.rawAmount !== asset.rawAmount; - } - - /** - * Persist the latest fetched assets and emit the corresponding keyring events. - * - * The input is treated as the latest snapshot for the account/network pairs - * included in this sync. The method compares that snapshot with the - * previously saved state to detect disappeared assets, emits asset-list - * updates, and emits balance updates including synthetic zero balances for - * assets that vanished from the latest response. - * - * @param assets - The latest asset snapshot returned by the refresh flow. - */ - async saveMany(assets: AssetEntity[]): Promise { - this.#logger.info('Saving assets', assets); - - const modesByNetwork = new Map(); - await Promise.all( - [...new Set(assets.map((asset) => asset.network))].map( - async (network) => { - modesByNetwork.set(network, await this.#resolveMode(network)); - }, - ), - ); - - const shouldEmitAsset = (asset: AssetEntity): boolean => - (modesByNetwork.get(asset.network) ?? 'snap') === 'snap' || - isSnapOwnedAsset(asset.assetType); - - const hasZeroAmount = (asset: AssetEntity): boolean => - asset.rawAmount === '0' || asset.uiAmount === '0'; - - const savedAssets = await this.getAll(); - const isEssentialAsset = (asset: AssetEntity): boolean => - ESSENTIAL_ASSETS.includes(asset.assetType); - - const isProtectedAsset = (asset: AssetEntity): boolean => { - const mode = modesByNetwork.get(asset.network) ?? 'snap'; - return mode === 'snap' - ? isEssentialAsset(asset) - : isSnapOwnedAsset(asset.assetType); + return { + ...result, + ...(await this.#coreAdapter.getAccountAssetsByIDs(accountId, fungibleIds)), }; + } - // Track only the account/network pairs refreshed in this run. - // That prevents us from treating assets from untouched networks as disappeared. - const syncedNetworksByAccount = assets.reduce>>( - (acc, asset) => { - acc[asset.keyringAccountId] ??= new Set(); - acc[asset.keyringAccountId]?.add(asset.network); - return acc; - }, - {}, - ); - - const incomingAssetKeys = new Set( - assets.map((asset) => `${asset.keyringAccountId}:${asset.assetType}`), + async getAccountAssetsByScope( + scope: Network, + accountId: string, + ): Promise { + const snapAssets = await this.#snapAdapter.getAccountAssetsByScope( + scope, + accountId, ); - - // A saved asset is considered disappeared only if its network was part of - // this sync, it is not essential, and it is missing from the latest - // snapshot for that account. - const disappearedAssets = savedAssets.filter((savedAsset) => { - const syncedNetworks = - syncedNetworksByAccount[savedAsset.keyringAccountId]; - - if ( - !syncedNetworks?.has(savedAsset.network) || - isProtectedAsset(savedAsset) - ) { - return false; - } - - const mode = modesByNetwork.get(savedAsset.network) ?? 'snap'; - if (mode === 'controller' && !isSnapOwnedAsset(savedAsset.assetType)) { - return false; - } - - return !incomingAssetKeys.has( - `${savedAsset.keyringAccountId}:${savedAsset.assetType}`, - ); - }); - - // A token should be removed from the visible asset list only when the latest - // snapshot says its balance is zero. Essential assets stay visible even at - // zero because they are part of the permanent Tron account model. - const shouldBeInRemovedList = (asset: AssetEntity): boolean => - hasZeroAmount(asset) && !isEssentialAsset(asset); // Never remove essential assets (including energy & bandwidth) from the account asset list - - // Assets are added to the visible list when they are non-zero and either: - // - we are doing a full non-incremental broadcast, or - // - they are brand new, or - // - they existed before with zero balance and now became non-zero. - const shouldBeInAddedList = (asset: AssetEntity): boolean => - !shouldBeInRemovedList(asset); - - // Build the asset-list payload in two stages: - // 1. seed the removed list with assets that vanished from the latest - // snapshot entirely - // 2. fold in the current assets to report additions and explicit zero-balance - // removals in the same event - const assetListUpdatedPayload = disappearedAssets - .filter(shouldEmitAsset) - .reduce( - (acc, asset) => ({ - ...acc, - [asset.keyringAccountId]: { - added: [...(acc[asset.keyringAccountId]?.added ?? [])], - removed: [ - ...(acc[asset.keyringAccountId]?.removed ?? []), - asset.assetType, - ], - }, - }), - {}, + const snapOwnedAssets = snapAssets.filter((asset) => + isSnapOwnedAsset(asset.assetType), ); + const stage = await this.#resolveMigrationStage(scope); - for (const asset of assets.filter(shouldEmitAsset)) { - // Merge the current snapshot into the pre-seeded payload so each account - // ends up with one consolidated added/removed diff. - assetListUpdatedPayload[asset.keyringAccountId] = { - added: [ - ...(assetListUpdatedPayload[asset.keyringAccountId]?.added ?? []), - ...(shouldBeInAddedList(asset) ? [asset.assetType] : []), - ], - removed: [ - ...(assetListUpdatedPayload[asset.keyringAccountId]?.removed ?? []), - ...(shouldBeInRemovedList(asset) ? [asset.assetType] : []), - ], - }; + if (stage === SnapsAssetsMigrationStage.Off) { + return snapAssets; } - // If no assets were added or removed, don't emit the event. - const isEmptyAccountAssetListUpdatedPayload = Object.values( - assetListUpdatedPayload, - ) - .map((item) => item.added.length + item.removed.length) - .every((item) => item === 0); - - if (!isEmptyAccountAssetListUpdatedPayload) { - await emitSnapKeyringEvent(snap, KeyringEvent.AccountAssetListUpdated, { - assets: assetListUpdatedPayload, - }); + if (stage === SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback) { + try { + const coreAssets = await this.#coreAdapter.getAccountAssetsByScope( + scope, + accountId, + ); + return [ + ...coreAssets.filter((asset) => !isSnapOwnedAsset(asset.assetType)), + ...snapOwnedAssets, + ]; + } catch { + return snapAssets; + } } - // Emit synthetic zero-balance entries for disappeared assets so clients can - // clear cached balances even when the backend omits zero-balance tokens - // instead of returning them explicitly. - const removedAssetsWithZeroBalance = disappearedAssets - .filter(shouldEmitAsset) - .map((asset) => ({ - ...asset, - rawAmount: '0', - uiAmount: '0', - })); - - const assetsToSave = [...assets, ...removedAssetsWithZeroBalance]; - // Save assets using repository - await this.#assetsRepository.saveMany(assetsToSave); - - // Broadcast the current snapshot plus synthetic zero-balance removals so the - // client can reconcile both visible assets and cached balances in one pass. - const balancesUpdatedPayload = [ - ...assets.filter(shouldEmitAsset), - ...removedAssetsWithZeroBalance, - ].reduce( - (acc, asset) => ({ - ...acc, - [asset.keyringAccountId]: { - ...(acc[asset.keyringAccountId] ?? {}), - [asset.assetType]: { - unit: asset.symbol, - amount: asset.uiAmount, - }, - }, - }), - {}, + const coreAssets = await this.#coreAdapter.getAccountAssetsByScope( + scope, + accountId, ); - - // Traverse the balancesUpdatedPayload object to check if we have at least 1 account that has at least 1 balance updated. - const isSomeBalanceChanged = Object.values(balancesUpdatedPayload) - .map((accountAssets) => Object.keys(accountAssets).length) // To each accountAssets object, map the number of assetTypes - .some((count) => count > 0); - - // Only emit the event if some balance was changed. - if (isSomeBalanceChanged) { - await emitSnapKeyringEvent(snap, KeyringEvent.AccountBalancesUpdated, { - balances: balancesUpdatedPayload, - }); - } - } - - async getAll(): Promise { - const assetsByAccount = - (await this.#state.getKey('assets')) ?? - {}; - - return Object.values(assetsByAccount).flat(); - } - - /** - * Creates an asset entity with zero balance from a known CAIP-19 asset ID. - * Uses pre-calculated metadata from TokenMetadata. - * - * @param assetId - The CAIP-19 asset ID (e.g., KnownCaip19Id.TrxMainnet). - * @param keyringAccountId - The keyring account ID. - * @returns The asset entity with zero balance. - */ - #createZeroBalanceAsset( - assetId: KnownCaip19Id, - keyringAccountId: string, - ): AssetEntity { - const metadata = TokenMetadata[assetId as keyof typeof TokenMetadata]; - const { chainId } = parseCaipAssetType(assetId); - - return { - assetType: metadata.id, - keyringAccountId, - network: chainId as Network, - symbol: metadata.symbol, - decimals: metadata.decimals, - rawAmount: '0', - uiAmount: '0', - } as AssetEntity; + return [ + ...coreAssets.filter((asset) => !isSnapOwnedAsset(asset.assetType)), + ...snapOwnedAssets, + ]; } - async getByKeyringAccountId( - keyringAccountId: string, + async fetchAssetsAndBalancesForAccount( + scope: Network, + account: KeyringAccount, ): Promise { - const savedAssets = - await this.#assetsRepository.getByAccountId(keyringAccountId); - - const networks = [ - ...new Set([ - ...savedAssets.map((asset) => asset.network), - ...ESSENTIAL_ASSETS.map( - (assetId) => - parseCaipAssetType(assetId as CaipAssetType).chainId as Network, - ), - ]), - ] as Network[]; - const modesByNetwork = new Map(); - await Promise.all( - networks.map(async (network) => { - modesByNetwork.set(network, await this.#resolveMode(network)); - }), - ); - - const visibleSavedAssets = savedAssets.filter((asset) => { - const mode = modesByNetwork.get(asset.network) ?? 'snap'; - return mode === 'snap' || isSnapOwnedAsset(asset.assetType); - }); - - /** - * Ensure the special assets are always present whether they have been synced or not. - * These are assets that should be visible to the user even with zero balance. - */ - const missingEssentialAssets: AssetEntity[] = []; - - for (const essentialAssetId of ESSENTIAL_ASSETS) { - const { chainId } = parseCaipAssetType(essentialAssetId as CaipAssetType); - const mode = modesByNetwork.get(chainId as Network) ?? 'snap'; - - if (mode === 'controller' && !isSnapOwnedAsset(essentialAssetId)) { - continue; - } - - const savedAsset = savedAssets.find( - (asset) => (asset.assetType as string) === essentialAssetId, - ); - - if (!savedAsset) { - const zeroBalanceAsset = this.#createZeroBalanceAsset( - essentialAssetId as KnownCaip19Id, - keyringAccountId, - ); - missingEssentialAssets.push(zeroBalanceAsset); - } - } - - return [...visibleSavedAssets, ...missingEssentialAssets]; + return this.#snapAdapter.fetchAssetsAndBalancesForAccount(scope, account); } - /** - * Extracts the ISO 4217 currency code (aka fiat ticker) from a fiat CAIP-19 asset type. - * - * @param caipAssetType - The CAIP-19 asset type. - * @returns The fiat ticker. - */ - #extractFiatTicker(caipAssetType: CaipAssetType): FiatTicker { - if (!AssetsService.isFiat(caipAssetType)) { - throw new Error('Passed caipAssetType is not a fiat asset'); - } - - const fiatTicker = - parseCaipAssetType(caipAssetType).assetReference.toLowerCase(); - - return fiatTicker as FiatTicker; + async saveMany(assets: AssetEntity[]): Promise { + return this.#snapAdapter.saveMany(assets); } - /** - * Fetches fiat exchange rates and crypto prices for the given assets. - * This is shared logic between getMultipleTokenConversions and getMultipleTokensMarketData. - * - * @param allAssets - Array of all CAIP asset types (both fiat and crypto). - * @returns Promise resolving to fiat exchange rates and crypto prices. - */ - async #fetchPriceData(allAssets: CaipAssetType[]): Promise<{ - fiatExchangeRates: Record; - cryptoPrices: Record; + async getHistoricalPrice( + from: CaipAssetType, + to: CaipAssetType, + ): Promise<{ + intervals: HistoricalPriceIntervals; + updateTime: number; + expirationTime?: number; }> { - const cryptoAssets = allAssets.filter( - (asset) => !AssetsService.isFiat(asset), - ); - - const [fiatExchangeRates, cryptoPrices] = await Promise.all([ - this.#priceApiClient.getFiatExchangeRates(), - this.#priceApiClient.getMultipleSpotPrices(cryptoAssets, 'usd'), - ]); - - return { fiatExchangeRates, cryptoPrices }; + return this.#snapAdapter.getHistoricalPrice(from, to); } - /** - * Get the token conversions for a list of asset pairs. - * It caches the results for 1 hour. - * - * Beware: Inside we are using the Price API's `getFiatExchangeRates` method for fiat prices, - * `getMultipleSpotPrices` for crypto prices and then using USD as an intermediate currency - * to convert the prices to the correct currency. This is not entirely accurate but it's the - * best we can do with the current API. - * - * @param conversions - The asset pairs to get the conversions for. - * @returns The token conversions. - */ async getMultipleTokenConversions( conversions: { from: CaipAssetType; to: CaipAssetType }[], ): Promise< Record> > { - if (conversions.length === 0) { - return {}; - } - - /** - * `from` and `to` can represent both fiat and crypto assets. For us to get their values - * the best approach is to use Price API's `getFiatExchangeRates` method for fiat prices, - * `getMultipleSpotPrices` for crypto prices and then using USD as an intermediate currency - * to convert the prices to the correct currency. - */ - const allAssets = conversions.flatMap((conversion) => [ - conversion.from, - conversion.to, - ]); - - const { fiatExchangeRates, cryptoPrices } = - await this.#fetchPriceData(allAssets); - - /** - * Now that we have the data, convert the `from`s to `to`s. - * - * We need to handle the following cases: - * 1. `from` and `to` are both fiat - * 2. `from` and `to` are both crypto - * 3. `from` is fiat and `to` is crypto - * 4. `from` is crypto and `to` is fiat - * - * We also need to keep in mind that although `cryptoPrices` are indexed - * by CAIP 19 IDs, the `fiatExchangeRates` are indexed by currency symbols. - * To convert fiat currency symbols to CAIP 19 IDs, we can use the - * `this.#fiatSymbolToCaip19Id` method. - */ - - const result: Record< - CaipAssetType, - Record - > = {}; - - conversions.forEach((conversion) => { - const { from, to } = conversion; - - result[from] ??= {}; - - let fromUsdRate: BigNumber; - let toUsdRate: BigNumber; - - if (AssetsService.isFiat(from)) { - /** - * Beware: - * We need to invert the fiat exchange rate because exchange rate != spot price - */ - const fiatExchangeRate = - fiatExchangeRates[this.#extractFiatTicker(from)]?.value; - - if (!fiatExchangeRate) { - result[from][to] = null; - return; - } - - fromUsdRate = new BigNumber(1).dividedBy(fiatExchangeRate); - } else { - fromUsdRate = new BigNumber(cryptoPrices[from]?.price ?? 0); - } - - if (AssetsService.isFiat(to)) { - /** - * Beware: - * We need to invert the fiat exchange rate because exchange rate != spot price - */ - const fiatExchangeRate = - fiatExchangeRates[this.#extractFiatTicker(to)]?.value; - - if (!fiatExchangeRate) { - result[from][to] = null; - return; - } - - toUsdRate = new BigNumber(1).dividedBy(fiatExchangeRate); - } else { - toUsdRate = new BigNumber(cryptoPrices[to]?.price ?? 0); - } - - if (fromUsdRate.isZero() || toUsdRate.isZero()) { - result[from][to] = null; - return; - } - - const rate = fromUsdRate.dividedBy(toUsdRate).toString(); - - const now = Date.now(); - - result[from][to] = { - rate, - conversionTime: now, - expirationTime: now + this.cacheTtlsMilliseconds.historicalPrices, - }; - }); - - return result; + return this.#snapAdapter.getMultipleTokenConversions(conversions); } - /** - * Computes the market data object in the target currency. - * - * @param spotPrice - The spot price of the asset in source currency. - * @param rate - The rate to convert the market data to from source currency to target currency. - * @returns The market data in the target currency. - */ - #computeMarketData( - spotPrice: SpotPrice, - rate: BigNumber, - ): FungibleAssetMarketData { - const marketDataInUsd = pick(spotPrice, [ - 'marketCap', - 'totalVolume', - 'circulatingSupply', - 'allTimeHigh', - 'allTimeLow', - 'pricePercentChange1h', - 'pricePercentChange1d', - 'pricePercentChange7d', - 'pricePercentChange14d', - 'pricePercentChange30d', - 'pricePercentChange200d', - 'pricePercentChange1y', - ]); - - const toCurrency = (value: number | null | undefined): string => { - return value === null || value === undefined - ? '' - : new BigNumber(value).dividedBy(rate).toString(); - }; - - const includeIfDefined = ( - key: string, - value: number | null | undefined, - ): Record => { - return value === null || value === undefined ? {} : { [key]: value }; - }; - - // Variations in percent don't need to be converted, they are independent of the currency - const pricePercentChange = { - ...includeIfDefined('PT1H', marketDataInUsd.pricePercentChange1h), - ...includeIfDefined('P1D', marketDataInUsd.pricePercentChange1d), - ...includeIfDefined('P7D', marketDataInUsd.pricePercentChange7d), - ...includeIfDefined('P14D', marketDataInUsd.pricePercentChange14d), - ...includeIfDefined('P30D', marketDataInUsd.pricePercentChange30d), - ...includeIfDefined('P200D', marketDataInUsd.pricePercentChange200d), - ...includeIfDefined('P1Y', marketDataInUsd.pricePercentChange1y), - }; - - const marketDataInToCurrency = { - fungible: true, - marketCap: toCurrency(marketDataInUsd.marketCap), - totalVolume: toCurrency(marketDataInUsd.totalVolume), - circulatingSupply: (marketDataInUsd.circulatingSupply ?? 0).toString(), // Circulating supply counts the number of tokens in circulation, so we don't convert - allTimeHigh: toCurrency(marketDataInUsd.allTimeHigh), - allTimeLow: toCurrency(marketDataInUsd.allTimeLow), - // Add pricePercentChange field only if it has values - ...(Object.keys(pricePercentChange).length > 0 - ? { pricePercentChange } - : {}), - } as FungibleAssetMarketData; - - return marketDataInToCurrency; + async getAssetsMetadata( + assetTypes: CaipAssetType[], + ): Promise> { + return this.#snapAdapter.getAssetsMetadata(assetTypes); } async getMultipleTokensMarketData( @@ -1831,143 +283,16 @@ export class AssetsService { ): Promise< Record> > { - if (assets.length === 0) { - return {}; - } - - /** - * `asset` and `unit` can represent both fiat and crypto assets. For us to get their values - * the best approach is to use Price API's `getFiatExchangeRates` method for fiat prices, - * `getMultipleSpotPrices` for crypto prices and then using USD as an intermediate currency - * to convert the prices to the correct currency. - */ - const allAssets = assets.flatMap((asset) => [asset.asset, asset.unit]); - - const { fiatExchangeRates, cryptoPrices } = - await this.#fetchPriceData(allAssets); - - const result: Record< - CaipAssetType, - Record - > = {}; - - assets.forEach((asset) => { - const { asset: assetType, unit } = asset; - - // Skip if we don't have price data for the asset - if (!cryptoPrices[assetType]) { - return; - } - - let unitUsdRate: BigNumber; - - if (AssetsService.isFiat(unit)) { - /** - * Beware: - * We need to invert the fiat exchange rate because exchange rate != spot price - */ - const fiatExchangeRate = - fiatExchangeRates[this.#extractFiatTicker(unit)]?.value; - - if (!fiatExchangeRate) { - return; - } - - unitUsdRate = new BigNumber(1).dividedBy(fiatExchangeRate); - } else { - unitUsdRate = new BigNumber(cryptoPrices[unit]?.price ?? 0); - } - - if (unitUsdRate.isZero()) { - return; - } - - // Initialize the nested structure for the asset if it doesn't exist - result[assetType] ??= {}; - - // Store the market data with the unit as the key - result[assetType][unit] = this.#computeMarketData( - cryptoPrices[assetType], - unitUsdRate, - ); - }); - - return result; + return this.#snapAdapter.getMultipleTokensMarketData(assets); } - /** - * Get historical prices for a token pair by calling the Price API. - * Similar to the Solana snap implementation. - * - * @param from - The asset to get historical prices for. - * @param to - The currency to convert prices to. - * @returns Historical price data with intervals. - */ - async getHistoricalPrice( - from: CaipAssetType, - to: CaipAssetType, - ): Promise<{ - intervals: HistoricalPriceIntervals; - updateTime: number; - expirationTime?: number; - }> { - assert(from, CaipAssetTypeStruct); - assert(to, CaipAssetTypeStruct); - - const toTicker = parseCaipAssetType(to).assetReference.toLowerCase(); - assert(toTicker, VsCurrencyParamStruct); - - const timePeriodsToFetch = ['1d', '7d', '1m', '3m', '1y', '1000y']; - - // For each time period, call the Price API to fetch the historical prices - const promises = timePeriodsToFetch.map(async (timePeriod) => - this.#priceApiClient - .getHistoricalPrices({ - assetType: from, - timePeriod, - vsCurrency: toTicker, - }) - // Wrap the response in an object with the time period and the response for easier reducing - .then((response) => ({ - timePeriod, - response, - })) - // Gracefully handle individual errors to avoid breaking the entire operation - .catch(async (error) => { - await this.#snapClient.trackError(error as Error); - this.#logger.warn( - `Error fetching historical prices for ${from} to ${to} with time period ${timePeriod}. Returning null object.`, - error, - ); - return { - timePeriod, - response: GET_HISTORICAL_PRICES_RESPONSE_NULL_OBJECT, - }; - }), - ); - - const wrappedHistoricalPrices = await Promise.all(promises); - - const intervals = wrappedHistoricalPrices.reduce( - (acc, { timePeriod, response }) => { - const iso8601Interval = `P${timePeriod.toUpperCase()}`; - acc[iso8601Interval] = response.prices.map((price) => [ - price[0], - price[1].toString(), - ]); - return acc; - }, - {}, - ); - - const now = Date.now(); - - const result = { - intervals, - updateTime: now, - expirationTime: now + this.cacheTtlsMilliseconds.historicalPrices, - }; + static isFiat(caipAssetId: CaipAssetType): boolean { + return SnapAssetsAdapter.isFiat(caipAssetId); + } - return result; + static hasChanged(asset: AssetEntity, assetsLookup: AssetEntity[]): boolean { + return SnapAssetsAdapter.hasChanged(asset, assetsLookup); } } + +export { SnapsAssetsMigrationStage }; diff --git a/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts b/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts new file mode 100644 index 00000000..4871b6b4 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts @@ -0,0 +1,99 @@ +import type { + AccountId, + Asset, + Caip19AssetId, +} from '@metamask/assets-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { CaipAssetType, CaipChainId } from '@metamask/utils'; +import { parseCaipAssetType } from '@metamask/utils'; + +import type { Network } from '../../../constants'; +import type { AssetEntity } from '../../../entities/assets'; +import type { CoreMessengerCaller } from '../../../types/core-messenger'; +import { mapControllerAsset } from '../mapControllerAsset'; + +/** + * Reads account assets from Core AssetsController via the Snap messenger endowment. + */ +export class CoreAssetsAdapter { + readonly #coreMessenger: CoreMessengerCaller; + + constructor({ coreMessenger }: { coreMessenger: CoreMessengerCaller }) { + this.#coreMessenger = coreMessenger; + } + + async getAccountAssetByID( + accountId: AccountId, + assetId: string, + ): Promise { + const result = await this.#coreMessenger.call( + 'AssetsController:getAsset', + accountId, + assetId as Caip19AssetId, + ); + + if (!result) { + return null; + } + + return mapControllerAsset(accountId, assetId, result); + } + + async getAccountAssetsByIDs( + accountId: AccountId, + assetIds: string[], + ): Promise> { + if (assetIds.length === 0) { + return {}; + } + + const chainIds = [ + ...new Set( + assetIds.map( + (assetId) => parseCaipAssetType(assetId as CaipAssetType).chainId, + ), + ), + ]; + + const controllerAssets = await this.#coreMessenger.call( + 'AssetsController:getAssets', + [{ id: accountId } as InternalAccount], + { chainIds }, + ); + + const accountAssets = + (controllerAssets as Record>)[accountId] ?? + {}; + + return Object.fromEntries( + assetIds.map((assetId) => { + const controllerAsset = accountAssets[assetId]; + return [ + assetId, + controllerAsset + ? mapControllerAsset(accountId, assetId, controllerAsset) + : null, + ]; + }), + ); + } + + async getAccountAssetsByScope( + scope: CaipChainId, + accountId: AccountId, + ): Promise { + const controllerAssets = await this.#coreMessenger.call( + 'AssetsController:getAssets', + [{ id: accountId } as InternalAccount], + { chainIds: [scope] }, + ); + + const accountAssets = + (controllerAssets as Record>)[accountId] ?? + {}; + + return Object.entries(accountAssets).map(([assetId, asset]) => + mapControllerAsset(accountId, assetId, asset), + ); + } +} diff --git a/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts b/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts new file mode 100644 index 00000000..11c4d508 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts @@ -0,0 +1,1914 @@ +import { KeyringEvent } from '@metamask/keyring-api'; +import type { + AccountAssetListUpdatedEvent, + AccountBalancesUpdatedEvent, + KeyringAccount, +} from '@metamask/keyring-api'; +import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; +import type { + AssetConversion, + AssetMetadata, + FungibleAssetMarketData, + FungibleAssetMetadata, + HistoricalPriceIntervals, +} from '@metamask/snaps-sdk'; +import { assert } from '@metamask/superstruct'; +import type { CaipAssetType } from '@metamask/utils'; +import { CaipAssetTypeStruct, parseCaipAssetType } from '@metamask/utils'; +import { BigNumber } from 'bignumber.js'; +import { pick } from 'lodash'; + +import type { PriceApiClient } from '../../../clients/price-api/PriceApiClient'; +import type { + FiatTicker, + SpotPrice, + SpotPrices, +} from '../../../clients/price-api/types'; +import { + GET_HISTORICAL_PRICES_RESPONSE_NULL_OBJECT, + VsCurrencyParamStruct, +} from '../../../clients/price-api/types'; +import type { SnapClient } from '../../../clients/snap/SnapClient'; +import type { TokenApiClient } from '../../../clients/token-api/TokenApiClient'; +import type { AccountResources } from '../../../clients/tron-http'; +import type { TronHttpClient } from '../../../clients/tron-http/TronHttpClient'; +import type { TrongridApiClient } from '../../../clients/trongrid/TrongridApiClient'; +import type { + RawTronUnfrozenV2, + Trc20Balance, + TronAccount, +} from '../../../clients/trongrid/types'; +import type { KnownCaip19Id, Network } from '../../../constants'; +import { + BANDWIDTH_METADATA, + ENERGY_METADATA, + ESSENTIAL_ASSETS, + MAX_BANDWIDTH_METADATA, + MAX_ENERGY_METADATA, + Networks, + TokenMetadata, + TRX_IN_LOCK_PERIOD_METADATA, + TRX_METADATA, + TRX_READY_FOR_WITHDRAWAL_METADATA, + TRX_STAKED_FOR_BANDWIDTH_METADATA, + TRX_STAKED_FOR_ENERGY_METADATA, + TRX_STAKING_REWARDS_METADATA, +} from '../../../constants'; +import { configProvider } from '../../../context'; +import type { AssetEntity } from '../../../entities/assets'; +import { toUiAmount } from '../../../utils/conversion'; +import { createPrefixedLogger } from '../../../utils/logger'; +import type { ILogger } from '../../../utils/logger'; +import type { State, UnencryptedStateValue } from '../../state/State'; +import { SnapsAssetsMigrationStage } from '@metamask/assets-controller'; + +import type { AssetsRepository } from '../AssetsRepository'; +import { isSnapOwnedAsset } from '../snapOwnedAssets'; +import type { + InLockPeriodCaipAssetType, + NativeCaipAssetType, + NftCaipAssetType, + ReadyForWithdrawalCaipAssetType, + ResourceCaipAssetType, + StakedCaipAssetType, + StakingRewardsCaipAssetType, + TokenCaipAssetType, +} from '../types'; + +/** + * Normalized account data structure that provides a consistent shape for both + * active and inactive accounts. This allows extraction functions to work + * without needing to know the account's activation state. + */ +type NormalizedAccountData = { + /** Native TRX balance in sun (0 for inactive accounts). */ + nativeBalance: number; + /** TRC10 token balances as `{ key: tokenId, value: balance }[]` (empty for inactive accounts). */ + trc10Balances: TronAccount['assetV2']; + /** TRC20 token balances from either account info or fallback endpoint. */ + trc20Balances: Trc20Balance[]; + /** Staking data including frozen balances and delegated resources. */ + stakedData: { + frozenV2: TronAccount['frozenV2']; + unfrozenV2: TronAccount['unfrozenV2']; + accountResource: TronAccount['account_resource'] | undefined; + }; + /** Account resources (energy, bandwidth). Empty object for inactive accounts. */ + resources: AccountResources | Record; + /** Unclaimed staking rewards in sun (0 if no rewards). */ + stakingRewards: number; +}; + +export class SnapAssetsAdapter { + readonly #logger: ILogger; + + readonly #assetsRepository: AssetsRepository; + + readonly #state: State; + + readonly #trongridApiClient: TrongridApiClient; + + readonly #tronHttpClient: TronHttpClient; + + readonly #priceApiClient: PriceApiClient; + + readonly #tokenApiClient: TokenApiClient; + + readonly #snapClient: SnapClient; + + readonly #resolveMigrationStage: ( + chainId: string, + ) => Promise; + + readonly cacheTtlsMilliseconds: { + fiatExchangeRates: number; + spotPrices: number; + historicalPrices: number; + }; + + constructor({ + logger, + assetsRepository, + state, + trongridApiClient, + tronHttpClient, + priceApiClient, + tokenApiClient, + snapClient, + resolveMigrationStage, + }: { + logger: ILogger; + assetsRepository: AssetsRepository; + state: State; + trongridApiClient: TrongridApiClient; + tronHttpClient: TronHttpClient; + priceApiClient: PriceApiClient; + tokenApiClient: TokenApiClient; + snapClient: SnapClient; + resolveMigrationStage: ( + chainId: string, + ) => Promise; + }) { + this.#logger = createPrefixedLogger(logger, '[🪙 SnapAssetsAdapter]'); + this.#assetsRepository = assetsRepository; + this.#state = state; + this.#trongridApiClient = trongridApiClient; + this.#tronHttpClient = tronHttpClient; + this.#priceApiClient = priceApiClient; + this.#tokenApiClient = tokenApiClient; + this.#snapClient = snapClient; + this.#resolveMigrationStage = resolveMigrationStage; + + const { cacheTtlsMilliseconds } = configProvider.get().priceApi; + this.cacheTtlsMilliseconds = cacheTtlsMilliseconds; + } + + static isFiat(caipAssetId: CaipAssetType): boolean { + return caipAssetId.includes('swift:0/iso4217:'); + } + + async #resolveStage(chainId: string): Promise { + return this.#resolveMigrationStage(chainId); + } + + async getAccountAssetByID( + accountId: string, + assetId: string, + ): Promise { + return this.#assetsRepository.getByAccountIdAndAssetType(accountId, assetId); + } + + async getAccountAssetsByIDs( + accountId: string, + assetIds: string[], + ): Promise> { + const assets = await this.#assetsRepository.getByAccountIdAndAssetTypes( + accountId, + assetIds, + ); + + return Object.fromEntries( + assetIds.map((assetId, index) => [assetId, assets[index] ?? null]), + ); + } + + /** + * Fetches all assets and balances for an account. + * + * Data Sources: + * - `getAccountInfoByAddress`: TRX balance, TRC10 tokens, TRC20 tokens (active accounts only) + * - `getAccountResources`: Energy and Bandwidth (returns {} for inactive accounts) + * - `getTrc20BalancesByAddress`: TRC20 balances fallback (works for inactive accounts) + * + * Logic Flow: + * 1. Fetch account info, resources, and TRC20 fallback (for inactive accounts) + * 2. Normalize data into consistent shape via `#buildAccountData` + * 3. Extract all assets via `#extractAssets` + * 4. Fetch metadata and prices in parallel + * 5. Enrich assets with metadata via `#enrichAssetsWithMetadata` + * 6. Filter spam tokens via `#filterTokensWithoutPriceData` + * + * @param scope - The network to query. + * @param account - The keyring account. + * @returns Promise - Array of assets with balances. + */ + async fetchAssetsAndBalancesForAccount( + scope: Network, + account: KeyringAccount, + ): Promise { + this.#logger.info('Fetching assets and balances by account', { + account, + scope, + }); + + const stage = await this.#resolveStage(scope); + + const [ + tronAccountInfoRequest, + tronAccountResourcesRequest, + stakingRewardsRequest, + ] = await Promise.allSettled([ + this.#trongridApiClient.getAccountInfoByAddress(scope, account.address), + this.#tronHttpClient.getAccountResources(scope, account.address), + this.#tronHttpClient.getReward(scope, account.address), + ]); + + const isInactiveAccount = tronAccountInfoRequest.status === 'rejected'; + if (isInactiveAccount) { + this.#logger.info( + 'Account info request failed, treating as inactive account', + { account, scope }, + ); + } + + const trc20BalancesFallback = + stage === SnapsAssetsMigrationStage.Off && isInactiveAccount + ? await this.#trongridApiClient + .getTrc20BalancesByAddress(scope, account.address) + .catch(async (error) => { + await this.#snapClient.trackError(error as Error); + this.#logger.warn( + 'Failed to fetch TRC20 balances for inactive account', + { error, account, scope }, + ); + return []; + }) + : []; + + const accountData = this.#buildAccountData({ + tronAccountInfoRequest, + tronAccountResourcesRequest, + trc20BalancesFallback, + stakingRewardsRequest, + }); + + const rawAssets = + stage !== SnapsAssetsMigrationStage.Off + ? this.#extractSnapOwnedAssets(account, scope, accountData) + : this.#extractAssets(account, scope, accountData); + + const assetTypes = rawAssets.map((asset) => asset.assetType); + const priceableAssetTypes = this.#getPriceableAssetTypes(rawAssets); + + const [assetsMetadata, spotPrices] = await Promise.all([ + this.getAssetsMetadata(assetTypes), + this.#priceApiClient + .getMultipleSpotPrices(priceableAssetTypes, 'usd') + .catch(async (error) => { + await this.#snapClient.trackError(error as Error); + return {}; + }), + ]); + + const enrichedAssets = this.#enrichAssetsWithMetadata( + rawAssets, + assetsMetadata, + ); + return this.#filterTokensWithoutPriceData(enrichedAssets, spotPrices); + } + + /** + * Filters out spam tokens (those without price data). + * Essential assets are always kept. Tokens need price data to be included. + * + * @param assets - The assets to filter. + * @param spotPrices - Pre-fetched USD prices for assets. + * @returns The filtered assets. + */ + #filterTokensWithoutPriceData( + assets: AssetEntity[], + spotPrices: SpotPrices | Record, + ): AssetEntity[] { + const filtered = assets.filter((asset) => { + // Essential assets (TRX, staked, energy, bandwidth) are always kept + if (ESSENTIAL_ASSETS.includes(asset.assetType)) { + return true; + } + // Tokens: keep only if they have price data + const spotPrice = (spotPrices as SpotPrices)[asset.assetType]; + return typeof spotPrice?.price === 'number'; + }); + + return filtered; + } + + /** + * Normalizes raw API responses into a consistent shape for both active and inactive accounts. + * This allows extraction functions to work without needing to know the account's activation state. + * + * @param params - The raw API responses to normalize. + * @param params.tronAccountInfoRequest - The settled promise result from getAccountInfoByAddress. + * @param params.tronAccountResourcesRequest - The settled promise result from getAccountResources. + * @param params.trc20BalancesFallback - TRC20 balances from fallback endpoint (empty for active accounts). + * @param params.stakingRewardsRequest - The settled promise result from getReward. + * @returns NormalizedAccountData - Consistent data shape for extraction. + */ + #buildAccountData({ + tronAccountInfoRequest, + tronAccountResourcesRequest, + trc20BalancesFallback, + stakingRewardsRequest, + }: { + tronAccountInfoRequest: PromiseSettledResult; + tronAccountResourcesRequest: PromiseSettledResult; + trc20BalancesFallback: Trc20Balance[]; + stakingRewardsRequest: PromiseSettledResult; + }): NormalizedAccountData { + const isInactiveAccount = tronAccountInfoRequest.status === 'rejected'; + const resources = + tronAccountResourcesRequest.status === 'fulfilled' + ? tronAccountResourcesRequest.value + : {}; + const stakingRewards = + stakingRewardsRequest.status === 'fulfilled' + ? Math.max(0, stakingRewardsRequest.value) + : 0; + + if (isInactiveAccount) { + return { + nativeBalance: 0, + trc10Balances: [], + trc20Balances: trc20BalancesFallback, + stakedData: { + frozenV2: [], + unfrozenV2: [], + accountResource: undefined, + }, + resources, + stakingRewards, + }; + } + + const tronAccountInfo = tronAccountInfoRequest.value; + return { + nativeBalance: tronAccountInfo.balance ?? 0, + trc10Balances: tronAccountInfo.assetV2 ?? [], + trc20Balances: tronAccountInfo.trc20 ?? [], + stakedData: { + frozenV2: tronAccountInfo.frozenV2 ?? [], + unfrozenV2: tronAccountInfo.unfrozenV2 ?? [], + accountResource: tronAccountInfo.account_resource, + }, + resources, + stakingRewards, + }; + } + + /** + * Extracts all assets from normalized account data. + * Coordinates calls to individual extraction functions. + * + * @param account - The keyring account. + * @param scope - The network. + * @param data - Normalized account data. + * @returns AssetEntity[] - Array of all extracted assets. + */ + #extractAssets( + account: KeyringAccount, + scope: Network, + data: NormalizedAccountData, + ): AssetEntity[] { + return [ + this.#extractNativeAsset(account, scope, data.nativeBalance), + ...this.#extractSnapOwnedAssets(account, scope, data), + ...this.#extractTrc10Assets(account, scope, data.trc10Balances), + ...this.#extractTrc20Assets(account, scope, data.trc20Balances), + ]; + } + + #extractSnapOwnedAssets( + account: KeyringAccount, + scope: Network, + data: NormalizedAccountData, + ): AssetEntity[] { + return [ + ...this.#extractStakedNativeAssets(account, scope, data.stakedData), + this.#extractReadyForWithdrawalAsset(account, scope, data.stakedData), + this.#extractInLockPeriodAsset(account, scope, data.stakedData), + this.#extractStakingRewardsAsset(account, scope, data.stakingRewards), + ...this.#extractBandwidth({ + account, + scope, + tronAccountResources: data.resources, + }), + ...this.#extractEnergy({ + account, + scope, + tronAccountResources: data.resources, + }), + ]; + } + + /** + * Returns the asset types that can be priced (native, TRC10, TRC20). + * Staked, energy, and bandwidth assets have non-compliant CAIP IDs that would fail the Price API. + * + * @param assets - Array of assets to filter. + * @returns CaipAssetType[] - Array of priceable asset types. + */ + #getPriceableAssetTypes(assets: AssetEntity[]): CaipAssetType[] { + return assets + .filter( + (asset) => + asset.assetType.includes('/slip44:') || + asset.assetType.includes('/trc10:') || + asset.assetType.includes('/trc20:'), + ) + .map((asset) => asset.assetType); + } + + /** + * Enriches assets with metadata (symbol, decimals, iconUrl) and calculates uiAmount. + * + * @param assets - Raw assets to enrich. + * @param assetsMetadata - Metadata lookup by asset type. + * @returns AssetEntity[] - Enriched assets. + */ + #enrichAssetsWithMetadata( + assets: AssetEntity[], + assetsMetadata: Record, + ): AssetEntity[] { + return assets.map((asset) => { + const metadata = assetsMetadata[ + asset.assetType + ] as FungibleAssetMetadata | null; + + const { + symbol: initialSymbol, + decimals: initialDecimals = 0, + iconUrl: initialIconUrl, + } = asset; + let symbol = initialSymbol; + let decimals = initialDecimals; + let iconUrl = initialIconUrl; + + if (metadata?.fungible) { + const unit = metadata.units?.[0]; + if (unit) { + symbol = unit.symbol ?? metadata.symbol ?? symbol; + decimals = unit.decimals ?? decimals; + } else { + symbol = metadata?.symbol ?? symbol; + } + iconUrl = metadata.iconUrl ?? iconUrl; + } + + const uiAmount = toUiAmount(asset.rawAmount, decimals).toString(); + + return { + ...asset, + symbol, + decimals, + uiAmount, + iconUrl, + }; + }); + } + + /** + * Extracts the native TRX asset from the balance. + * + * @param account - The keyring account. + * @param scope - The network. + * @param balance - The native balance in sun. + * @returns AssetEntity - The native TRX asset. + */ + #extractNativeAsset( + account: KeyringAccount, + scope: Network, + balance: number, + ): AssetEntity { + return { + assetType: Networks[scope].nativeToken.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].nativeToken.symbol, + decimals: Networks[scope].nativeToken.decimals, + rawAmount: balance.toString(), + uiAmount: toUiAmount( + balance, + Networks[scope].nativeToken.decimals, + ).toString(), + iconUrl: Networks[scope].nativeToken.iconUrl, + }; + } + + /** + * Extracts staked TRX assets (for bandwidth and energy). + * + * @param account - The keyring account. + * @param scope - The network. + * @param stakedData - Staking data including frozen balances and delegated resources. + * @returns AssetEntity[] - Array of staked assets (always 2: bandwidth and energy, amounts may be 0). + */ + #extractStakedNativeAssets( + account: KeyringAccount, + scope: Network, + stakedData: NormalizedAccountData['stakedData'], + ): AssetEntity[] { + const assets: AssetEntity[] = []; + + let stakedBandwidthAmount = 0; + let stakedEnergyAmount = 0; + + stakedData.frozenV2?.forEach((frozen) => { + const amount = frozen.amount ?? 0; + + if (frozen.type === 'ENERGY') { + stakedEnergyAmount += amount; + } else if (!frozen.type) { + // Item without type is for bandwidth + stakedBandwidthAmount += amount; + } + }); + + const delegatedBandwidth = + stakedData.accountResource?.delegated_frozenV2_balance_for_bandwidth ?? 0; + const delegatedEnergy = + stakedData.accountResource?.delegated_frozenV2_balance_for_energy ?? 0; + + stakedBandwidthAmount += delegatedBandwidth; + stakedEnergyAmount += delegatedEnergy; + + assets.push({ + assetType: Networks[scope].stakedForBandwidth.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].stakedForBandwidth.symbol, + decimals: Networks[scope].stakedForBandwidth.decimals, + rawAmount: stakedBandwidthAmount.toString(), + uiAmount: toUiAmount( + stakedBandwidthAmount, + Networks[scope].stakedForBandwidth.decimals, + ).toString(), + iconUrl: Networks[scope].stakedForBandwidth.iconUrl, + }); + + assets.push({ + assetType: Networks[scope].stakedForEnergy.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].stakedForEnergy.symbol, + decimals: Networks[scope].stakedForEnergy.decimals, + rawAmount: stakedEnergyAmount.toString(), + uiAmount: toUiAmount( + stakedEnergyAmount, + Networks[scope].stakedForEnergy.decimals, + ).toString(), + iconUrl: Networks[scope].stakedForEnergy.iconUrl, + }); + + return assets; + } + + /** + * Extracts TRX ready for withdrawal (unstaked TRX that has completed the withdrawal period). + * + * @param account - The keyring account. + * @param scope - The network. + * @param stakedData - Staking data including unfrozen balances. + * @returns AssetEntity - The ready-for-withdrawal asset (amount may be 0). + */ + #extractReadyForWithdrawalAsset( + account: KeyringAccount, + scope: Network, + stakedData: NormalizedAccountData['stakedData'], + ): AssetEntity { + const currentTimestamp = Date.now(); + let readyForWithdrawalAmount = 0; + + stakedData.unfrozenV2?.forEach((unfrozen: RawTronUnfrozenV2) => { + const expireTime = unfrozen.unfreeze_expire_time ?? 0; + const amount = unfrozen.unfreeze_amount ?? 0; + + if (expireTime <= currentTimestamp && amount > 0) { + readyForWithdrawalAmount += amount; + } + }); + + const { id, symbol, decimals, iconUrl } = + Networks[scope].readyForWithdrawal; + + return { + assetType: id, + keyringAccountId: account.id, + network: scope, + symbol, + decimals, + rawAmount: readyForWithdrawalAmount.toString(), + uiAmount: toUiAmount(readyForWithdrawalAmount, decimals).toString(), + iconUrl, + }; + } + + /** + * Extracts staking rewards asset (unclaimed voting rewards). + * + * @param account - The keyring account. + * @param scope - The network. + * @param stakingRewards - Unclaimed staking rewards in sun. + * @returns AssetEntity - The staking rewards asset. + */ + #extractStakingRewardsAsset( + account: KeyringAccount, + scope: Network, + stakingRewards: number, + ): AssetEntity { + return { + assetType: Networks[scope].stakingRewards.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].stakingRewards.symbol, + decimals: Networks[scope].stakingRewards.decimals, + rawAmount: stakingRewards.toString(), + uiAmount: toUiAmount( + stakingRewards, + Networks[scope].stakingRewards.decimals, + ).toString(), + iconUrl: Networks[scope].stakingRewards.iconUrl, + }; + } + + /** + * Extracts TRX that is in the lock period (unstaked but lock period not yet ended). + * This represents TRX that the user has initiated unstaking for but must wait + * the 14-day lock period before they can withdraw. + * + * @param account - The keyring account. + * @param scope - The network. + * @param stakedData - Staking data including unfrozen balances. + * @returns AssetEntity - The in-lock-period asset (amount may be 0). + */ + #extractInLockPeriodAsset( + account: KeyringAccount, + scope: Network, + stakedData: NormalizedAccountData['stakedData'], + ): AssetEntity { + const currentTimestamp = Date.now(); + let inLockPeriodAmount = 0; + + stakedData.unfrozenV2?.forEach((unfrozen: RawTronUnfrozenV2) => { + const expireTime = unfrozen.unfreeze_expire_time ?? 0; + const amount = unfrozen.unfreeze_amount ?? 0; + + if (expireTime > currentTimestamp && amount > 0) { + inLockPeriodAmount += amount; + } + }); + + const { id, symbol, decimals, iconUrl } = Networks[scope].inLockPeriod; + + return { + assetType: id, + keyringAccountId: account.id, + network: scope, + symbol, + decimals, + rawAmount: inLockPeriodAmount.toString(), + uiAmount: toUiAmount(inLockPeriodAmount, decimals).toString(), + iconUrl, + }; + } + + /** + * Extracts current and maximum bandwidth from the account resources. + * + * @param options - Options object. + * @param options.account - The account to extract bandwidth for. + * @param options.scope - The network to extract bandwidth for. + * @param options.tronAccountResources - The account resources to extract bandwidth for. + * @returns The bandwidth assets. + */ + #extractBandwidth({ + account, + scope, + tronAccountResources, + }: { + account: KeyringAccount; + scope: Network; + tronAccountResources: AccountResources | Record; + }): AssetEntity[] { + const freeBandwidth = tronAccountResources?.freeNetLimit ?? 0; + const stakingBandwidth = tronAccountResources?.NetLimit ?? 0; + const maximumBandwidth = freeBandwidth + stakingBandwidth; + + const usedFreeBandwidth = tronAccountResources?.freeNetUsed ?? 0; + const usedStakingBandwidth = tronAccountResources?.NetUsed ?? 0; + const usedBandwidth = usedFreeBandwidth + usedStakingBandwidth; + + const availableBandwidth = Math.max(0, maximumBandwidth - usedBandwidth); + + return [ + { + assetType: Networks[scope].bandwidth.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].bandwidth.symbol, + decimals: Networks[scope].bandwidth.decimals, + rawAmount: availableBandwidth.toString(), + uiAmount: availableBandwidth.toString(), + iconUrl: Networks[scope].bandwidth.iconUrl, + }, + { + assetType: Networks[scope].maximumBandwidth.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].maximumBandwidth.symbol, + decimals: Networks[scope].maximumBandwidth.decimals, + rawAmount: maximumBandwidth.toString(), + uiAmount: maximumBandwidth.toString(), + iconUrl: Networks[scope].maximumBandwidth.iconUrl, + }, + ]; + } + + /** + * Extracts current and maximum energy from the account resources. + * + * @param options - Options object. + * @param options.account - The keyring account. + * @param options.scope - The network. + * @param options.tronAccountResources - Account resources (energy, bandwidth). + * @returns AssetEntity[] - Array containing energy and maximum energy assets. + */ + #extractEnergy({ + account, + scope, + tronAccountResources, + }: { + account: KeyringAccount; + scope: Network; + tronAccountResources: AccountResources | Record; + }): AssetEntity[] { + const maximumEnergy = tronAccountResources?.EnergyLimit ?? 0; + const usedEnergy = tronAccountResources?.EnergyUsed ?? 0; + + /** + * We might have used more Energy than the maximum allocated + */ + const availableEnergy = Math.max(0, maximumEnergy - usedEnergy); + + return [ + { + assetType: Networks[scope].energy.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].energy.symbol, + decimals: Networks[scope].energy.decimals, + rawAmount: availableEnergy.toString(), + uiAmount: availableEnergy.toString(), + iconUrl: Networks[scope].energy.iconUrl, + }, + { + assetType: Networks[scope].maximumEnergy.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].maximumEnergy.symbol, + decimals: Networks[scope].maximumEnergy.decimals, + rawAmount: maximumEnergy.toString(), + uiAmount: maximumEnergy.toString(), + iconUrl: Networks[scope].maximumEnergy.iconUrl, + }, + ]; + } + + /** + * Extracts TRC10 assets from the balances array. + * + * @param account - The keyring account. + * @param scope - The network. + * @param trc10Balances - TRC10 token balances as `{ key: tokenId, value: balance }[]`. + * @returns AssetEntity[] - Array of TRC10 asset entities. + */ + #extractTrc10Assets( + account: KeyringAccount, + scope: Network, + trc10Balances: TronAccount['assetV2'], + ): AssetEntity[] { + return ( + trc10Balances?.flatMap((tokenObject) => { + // assetV2 has structure: { "key": "token_id", "value": "balance" } + return { + assetType: `${scope}/trc10:${tokenObject.key}` as TokenCaipAssetType, + keyringAccountId: account.id, + network: scope, + symbol: '', + decimals: 0, + rawAmount: tokenObject.value?.toString() ?? '0', + uiAmount: '0', + iconUrl: '', // Will be enriched with metadata later + }; + }) ?? [] + ); + } + + /** + * Extracts TRC20 assets from a balances array. + * Works with both active accounts (tronAccountInfo.trc20) and inactive accounts (getTrc20BalancesByAddress). + * + * @param account - The keyring account. + * @param scope - The network. + * @param trc20Balances - Array of `Record` objects (e.g., `[{ "TContractAddr": "1000" }]`). + * @returns AssetEntity[] - Array of TRC20 asset entities. + */ + #extractTrc20Assets( + account: KeyringAccount, + scope: Network, + trc20Balances: Trc20Balance[], + ): AssetEntity[] { + return trc20Balances.flatMap((tokenObject) => { + return Object.entries(tokenObject).map(([address, balance]) => { + return { + assetType: `${scope}/trc20:${address}` as TokenCaipAssetType, + keyringAccountId: account.id, + network: scope, + symbol: '', + decimals: 0, + rawAmount: balance, + uiAmount: '0', + iconUrl: '', // Will be enriched with metadata later + }; + }); + }); + } + + async getAssetsMetadata( + assetTypes: CaipAssetType[], + ): Promise> { + this.#logger.info('Fetching metadata for assets', assetTypes); + + const { + nativeAssetTypes, + stakedNativeAssetTypes, + readyForWithdrawalAssetTypes, + inLockPeriodAssetTypes, + stakingRewardsAssetTypes, + energyAssetTypes, + maximunEnergyAssetTypes, + bandwidthAssetTypes, + maximunBandwidthAssetTypes, + tokenTrc10AssetTypes, + tokenTrc20AssetTypes, + } = this.#splitAssetsByType(assetTypes); + + const nativeTokensMetadata = + this.#getNativeTokensMetadata(nativeAssetTypes); + const stakedTokensMetadata = this.#getStakedTokensMetadata( + stakedNativeAssetTypes, + ); + const readyForWithdrawalTokensMetadata = + this.#getReadyForWithdrawalTokensMetadata(readyForWithdrawalAssetTypes); + const inLockPeriodTokensMetadata = this.#getInLockPeriodMetadata( + inLockPeriodAssetTypes, + ); + const stakingRewardsMetadata = this.#getStakingRewardsMetadata( + stakingRewardsAssetTypes, + ); + const energyTokensMetadata = this.#getEnergyMetadata(energyAssetTypes); + const maximunEnergyTokensMetadata = this.#getMaximunEnergyMetadata( + maximunEnergyAssetTypes, + ); + const bandwidthTokensMetadata = + this.#getBandwidthMetadata(bandwidthAssetTypes); + const maximunBandwidthTokensMetadata = this.#getMaximunBandwidthMetadata( + maximunBandwidthAssetTypes, + ); + const tokensMetadata = await this.#getTokensMetadata([ + ...tokenTrc10AssetTypes, + ...tokenTrc20AssetTypes, + ]); + + const result = { + ...nativeTokensMetadata, + ...stakedTokensMetadata, + ...readyForWithdrawalTokensMetadata, + ...inLockPeriodTokensMetadata, + ...stakingRewardsMetadata, + ...energyTokensMetadata, + ...maximunEnergyTokensMetadata, + ...bandwidthTokensMetadata, + ...maximunBandwidthTokensMetadata, + ...tokensMetadata, + }; + + this.#logger.info('Resolved assets metadata', { assetTypes, result }); + + return result; + } + + #splitAssetsByType(assetTypes: CaipAssetType[]): { + nativeAssetTypes: NativeCaipAssetType[]; + stakedNativeAssetTypes: StakedCaipAssetType[]; + readyForWithdrawalAssetTypes: ReadyForWithdrawalCaipAssetType[]; + inLockPeriodAssetTypes: InLockPeriodCaipAssetType[]; + stakingRewardsAssetTypes: StakingRewardsCaipAssetType[]; + energyAssetTypes: ResourceCaipAssetType[]; + maximunEnergyAssetTypes: ResourceCaipAssetType[]; + bandwidthAssetTypes: ResourceCaipAssetType[]; + maximunBandwidthAssetTypes: ResourceCaipAssetType[]; + tokenTrc10AssetTypes: TokenCaipAssetType[]; + tokenTrc20AssetTypes: TokenCaipAssetType[]; + nftAssetTypes: NftCaipAssetType[]; + } { + const nativeAssetTypes = assetTypes.filter((assetType) => + assetType.endsWith('/slip44:195'), + ) as NativeCaipAssetType[]; + const stakedNativeAssetTypes = assetTypes.filter((assetType) => + assetType.includes('/slip44:195-staked-for-'), + ) as StakedCaipAssetType[]; + const readyForWithdrawalAssetTypes = assetTypes.filter((assetType) => + assetType.endsWith('/slip44:195-ready-for-withdrawal'), + ) as ReadyForWithdrawalCaipAssetType[]; + const inLockPeriodAssetTypes = assetTypes.filter((assetType) => + assetType.endsWith('/slip44:195-in-lock-period'), + ) as InLockPeriodCaipAssetType[]; + const stakingRewardsAssetTypes = assetTypes.filter((assetType) => + assetType.endsWith('/slip44:195-staking-rewards'), + ) as StakingRewardsCaipAssetType[]; + const energyAssetTypes = assetTypes.filter((assetType) => + assetType.endsWith('/slip44:energy'), + ) as ResourceCaipAssetType[]; + const maximunEnergyAssetTypes = assetTypes.filter((assetType) => + assetType.endsWith('/slip44:maximum-energy'), + ) as ResourceCaipAssetType[]; + const bandwidthAssetTypes = assetTypes.filter((assetType) => + assetType.endsWith('/slip44:bandwidth'), + ) as ResourceCaipAssetType[]; + const maximunBandwidthAssetTypes = assetTypes.filter((assetType) => + assetType.endsWith('/slip44:maximum-bandwidth'), + ) as ResourceCaipAssetType[]; + const tokenTrc10AssetTypes = assetTypes.filter((assetType) => + assetType.includes('/trc10:'), + ) as TokenCaipAssetType[]; + const tokenTrc20AssetTypes = assetTypes.filter((assetType) => + assetType.includes('/trc20:'), + ) as TokenCaipAssetType[]; + const nftAssetTypes = assetTypes.filter((assetType) => + assetType.includes('/trc721:'), + ) as NftCaipAssetType[]; + + return { + nativeAssetTypes, + stakedNativeAssetTypes, + readyForWithdrawalAssetTypes, + inLockPeriodAssetTypes, + stakingRewardsAssetTypes, + energyAssetTypes, + maximunEnergyAssetTypes, + bandwidthAssetTypes, + maximunBandwidthAssetTypes, + tokenTrc10AssetTypes, + tokenTrc20AssetTypes, + nftAssetTypes, + }; + } + + #getNativeTokensMetadata( + assetTypes: NativeCaipAssetType[], + ): Record { + const nativeTokensMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + nativeTokensMetadata[assetType] = { + fungible: TRX_METADATA.fungible, + name: TRX_METADATA.name, + symbol: TRX_METADATA.symbol, + iconUrl: TRX_METADATA.iconUrl, + units: [ + { + decimals: TRX_METADATA.decimals, + symbol: TRX_METADATA.symbol, + name: TRX_METADATA.name, + }, + ], + }; + } + + return nativeTokensMetadata; + } + + #getStakedTokensMetadata( + assetTypes: StakedCaipAssetType[], + ): Record { + // Can either be Staked for Bandwidth or Staked for Energy + const stakedTokensMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + const isForBandwdidth = assetType.endsWith('staked-for-bandwidth'); + + if (isForBandwdidth) { + stakedTokensMetadata[assetType] = { + fungible: TRX_STAKED_FOR_BANDWIDTH_METADATA.fungible, + name: TRX_STAKED_FOR_BANDWIDTH_METADATA.name, + symbol: TRX_STAKED_FOR_BANDWIDTH_METADATA.symbol, + iconUrl: TRX_STAKED_FOR_BANDWIDTH_METADATA.iconUrl, + units: [ + { + decimals: TRX_STAKED_FOR_BANDWIDTH_METADATA.decimals, + symbol: TRX_STAKED_FOR_BANDWIDTH_METADATA.symbol, + name: TRX_STAKED_FOR_BANDWIDTH_METADATA.name, + }, + ], + }; + } + + const isForEnergy = assetType.endsWith('staked-for-energy'); + + if (isForEnergy) { + stakedTokensMetadata[assetType] = { + fungible: TRX_STAKED_FOR_ENERGY_METADATA.fungible, + name: TRX_STAKED_FOR_ENERGY_METADATA.name, + symbol: TRX_STAKED_FOR_ENERGY_METADATA.symbol, + iconUrl: TRX_STAKED_FOR_ENERGY_METADATA.iconUrl, + units: [ + { + decimals: TRX_STAKED_FOR_ENERGY_METADATA.decimals, + symbol: TRX_STAKED_FOR_ENERGY_METADATA.symbol, + name: TRX_STAKED_FOR_ENERGY_METADATA.name, + }, + ], + }; + } + } + + return stakedTokensMetadata; + } + + #getReadyForWithdrawalTokensMetadata( + assetTypes: ReadyForWithdrawalCaipAssetType[], + ): Record { + const readyForWithdrawalTokensMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + readyForWithdrawalTokensMetadata[assetType] = { + fungible: TRX_READY_FOR_WITHDRAWAL_METADATA.fungible, + name: TRX_READY_FOR_WITHDRAWAL_METADATA.name, + symbol: TRX_READY_FOR_WITHDRAWAL_METADATA.symbol, + iconUrl: TRX_READY_FOR_WITHDRAWAL_METADATA.iconUrl, + units: [ + { + decimals: TRX_READY_FOR_WITHDRAWAL_METADATA.decimals, + symbol: TRX_READY_FOR_WITHDRAWAL_METADATA.symbol, + name: TRX_READY_FOR_WITHDRAWAL_METADATA.name, + }, + ], + }; + } + + return readyForWithdrawalTokensMetadata; + } + + #getStakingRewardsMetadata( + assetTypes: StakingRewardsCaipAssetType[], + ): Record { + const stakingRewardsMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + stakingRewardsMetadata[assetType] = { + fungible: TRX_STAKING_REWARDS_METADATA.fungible, + name: TRX_STAKING_REWARDS_METADATA.name, + symbol: TRX_STAKING_REWARDS_METADATA.symbol, + iconUrl: TRX_STAKING_REWARDS_METADATA.iconUrl, + units: [ + { + decimals: TRX_STAKING_REWARDS_METADATA.decimals, + symbol: TRX_STAKING_REWARDS_METADATA.symbol, + name: TRX_STAKING_REWARDS_METADATA.name, + }, + ], + }; + } + + return stakingRewardsMetadata; + } + + #getInLockPeriodMetadata( + assetTypes: InLockPeriodCaipAssetType[], + ): Record { + const inLockPeriodTokensMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + inLockPeriodTokensMetadata[assetType] = { + fungible: TRX_IN_LOCK_PERIOD_METADATA.fungible, + name: TRX_IN_LOCK_PERIOD_METADATA.name, + symbol: TRX_IN_LOCK_PERIOD_METADATA.symbol, + iconUrl: TRX_IN_LOCK_PERIOD_METADATA.iconUrl, + units: [ + { + decimals: TRX_IN_LOCK_PERIOD_METADATA.decimals, + symbol: TRX_IN_LOCK_PERIOD_METADATA.symbol, + name: TRX_IN_LOCK_PERIOD_METADATA.name, + }, + ], + }; + } + + return inLockPeriodTokensMetadata; + } + + #getBandwidthMetadata( + assetTypes: ResourceCaipAssetType[], + ): Record { + const bandwidthTokensMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + bandwidthTokensMetadata[assetType] = { + fungible: BANDWIDTH_METADATA.fungible, + name: BANDWIDTH_METADATA.name, + symbol: BANDWIDTH_METADATA.symbol, + iconUrl: BANDWIDTH_METADATA.iconUrl, + units: [ + { + decimals: BANDWIDTH_METADATA.decimals, + symbol: BANDWIDTH_METADATA.symbol, + name: BANDWIDTH_METADATA.name, + }, + ], + }; + } + + return bandwidthTokensMetadata; + } + + #getMaximunBandwidthMetadata( + assetTypes: ResourceCaipAssetType[], + ): Record { + const maximunBandwidthTokensMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + maximunBandwidthTokensMetadata[assetType] = { + fungible: MAX_BANDWIDTH_METADATA.fungible, + name: MAX_BANDWIDTH_METADATA.name, + symbol: MAX_BANDWIDTH_METADATA.symbol, + iconUrl: MAX_BANDWIDTH_METADATA.iconUrl, + units: [ + { + decimals: MAX_BANDWIDTH_METADATA.decimals, + symbol: MAX_BANDWIDTH_METADATA.symbol, + name: MAX_BANDWIDTH_METADATA.name, + }, + ], + }; + } + + return maximunBandwidthTokensMetadata; + } + + #getEnergyMetadata( + assetTypes: ResourceCaipAssetType[], + ): Record { + const energyTokensMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + energyTokensMetadata[assetType] = { + fungible: ENERGY_METADATA.fungible, + name: ENERGY_METADATA.name, + symbol: ENERGY_METADATA.symbol, + iconUrl: ENERGY_METADATA.iconUrl, + units: [ + { + decimals: ENERGY_METADATA.decimals, + symbol: ENERGY_METADATA.symbol, + name: ENERGY_METADATA.name, + }, + ], + }; + } + + return energyTokensMetadata; + } + + #getMaximunEnergyMetadata( + assetTypes: ResourceCaipAssetType[], + ): Record { + const maximunEnergyTokensMetadata: Record< + CaipAssetType, + FungibleAssetMetadata | null + > = {}; + + for (const assetType of assetTypes) { + maximunEnergyTokensMetadata[assetType] = { + fungible: MAX_ENERGY_METADATA.fungible, + name: MAX_ENERGY_METADATA.name, + symbol: MAX_ENERGY_METADATA.symbol, + iconUrl: MAX_ENERGY_METADATA.iconUrl, + units: [ + { + decimals: MAX_ENERGY_METADATA.decimals, + symbol: MAX_ENERGY_METADATA.symbol, + name: MAX_ENERGY_METADATA.name, + }, + ], + }; + } + + return maximunEnergyTokensMetadata; + } + + async #getTokensMetadata( + assetTypes: TokenCaipAssetType[], + ): Promise> { + return this.#tokenApiClient.getTokensMetadata(assetTypes); + } + + /** + * Checks if the asset has changed compared to passed assets lookup. + * + * @param asset - The asset to check. + * @param assetsLookup - The lookup table to check against. + * @returns True if the asset has changed, false otherwise. + */ + static hasChanged(asset: AssetEntity, assetsLookup: AssetEntity[]): boolean { + const savedAsset = assetsLookup.find( + (item) => + item.keyringAccountId === asset.keyringAccountId && + item.assetType === asset.assetType, + ); + + if (!savedAsset) { + return true; + } + + return savedAsset.rawAmount !== asset.rawAmount; + } + + /** + * Persist the latest fetched assets and emit the corresponding keyring events. + * + * The input is treated as the latest snapshot for the account/network pairs + * included in this sync. The method compares that snapshot with the + * previously saved state to detect disappeared assets, emits asset-list + * updates, and emits balance updates including synthetic zero balances for + * assets that vanished from the latest response. + * + * @param assets - The latest asset snapshot returned by the refresh flow. + */ + async saveMany(assets: AssetEntity[]): Promise { + this.#logger.info('Saving assets', assets); + + const stagesByNetwork = new Map(); + await Promise.all( + [...new Set(assets.map((asset) => asset.network))].map( + async (network) => { + stagesByNetwork.set(network, await this.#resolveStage(network)); + }, + ), + ); + + const shouldEmitAsset = (asset: AssetEntity): boolean => + (stagesByNetwork.get(asset.network) ?? SnapsAssetsMigrationStage.Off) === + SnapsAssetsMigrationStage.Off || isSnapOwnedAsset(asset.assetType); + + const hasZeroAmount = (asset: AssetEntity): boolean => + asset.rawAmount === '0' || asset.uiAmount === '0'; + + const savedAssets = await this.getAll(); + const isEssentialAsset = (asset: AssetEntity): boolean => + ESSENTIAL_ASSETS.includes(asset.assetType); + + const isProtectedAsset = (asset: AssetEntity): boolean => { + const stage = + stagesByNetwork.get(asset.network) ?? SnapsAssetsMigrationStage.Off; + return stage === SnapsAssetsMigrationStage.Off + ? isEssentialAsset(asset) + : isSnapOwnedAsset(asset.assetType); + }; + + // Track only the account/network pairs refreshed in this run. + // That prevents us from treating assets from untouched networks as disappeared. + const syncedNetworksByAccount = assets.reduce>>( + (acc, asset) => { + acc[asset.keyringAccountId] ??= new Set(); + acc[asset.keyringAccountId]?.add(asset.network); + return acc; + }, + {}, + ); + + const incomingAssetKeys = new Set( + assets.map((asset) => `${asset.keyringAccountId}:${asset.assetType}`), + ); + + // A saved asset is considered disappeared only if its network was part of + // this sync, it is not essential, and it is missing from the latest + // snapshot for that account. + const disappearedAssets = savedAssets.filter((savedAsset) => { + const syncedNetworks = + syncedNetworksByAccount[savedAsset.keyringAccountId]; + + if ( + !syncedNetworks?.has(savedAsset.network) || + isProtectedAsset(savedAsset) + ) { + return false; + } + + const stage = + stagesByNetwork.get(savedAsset.network) ?? SnapsAssetsMigrationStage.Off; + if ( + stage !== SnapsAssetsMigrationStage.Off && + !isSnapOwnedAsset(savedAsset.assetType) + ) { + return false; + } + + return !incomingAssetKeys.has( + `${savedAsset.keyringAccountId}:${savedAsset.assetType}`, + ); + }); + + // A token should be removed from the visible asset list only when the latest + // snapshot says its balance is zero. Essential assets stay visible even at + // zero because they are part of the permanent Tron account model. + const shouldBeInRemovedList = (asset: AssetEntity): boolean => + hasZeroAmount(asset) && !isEssentialAsset(asset); // Never remove essential assets (including energy & bandwidth) from the account asset list + + // Assets are added to the visible list when they are non-zero and either: + // - we are doing a full non-incremental broadcast, or + // - they are brand new, or + // - they existed before with zero balance and now became non-zero. + const shouldBeInAddedList = (asset: AssetEntity): boolean => + !shouldBeInRemovedList(asset); + + // Build the asset-list payload in two stages: + // 1. seed the removed list with assets that vanished from the latest + // snapshot entirely + // 2. fold in the current assets to report additions and explicit zero-balance + // removals in the same event + const assetListUpdatedPayload = disappearedAssets + .filter(shouldEmitAsset) + .reduce( + (acc, asset) => ({ + ...acc, + [asset.keyringAccountId]: { + added: [...(acc[asset.keyringAccountId]?.added ?? [])], + removed: [ + ...(acc[asset.keyringAccountId]?.removed ?? []), + asset.assetType, + ], + }, + }), + {}, + ); + + for (const asset of assets.filter(shouldEmitAsset)) { + // Merge the current snapshot into the pre-seeded payload so each account + // ends up with one consolidated added/removed diff. + assetListUpdatedPayload[asset.keyringAccountId] = { + added: [ + ...(assetListUpdatedPayload[asset.keyringAccountId]?.added ?? []), + ...(shouldBeInAddedList(asset) ? [asset.assetType] : []), + ], + removed: [ + ...(assetListUpdatedPayload[asset.keyringAccountId]?.removed ?? []), + ...(shouldBeInRemovedList(asset) ? [asset.assetType] : []), + ], + }; + } + + // If no assets were added or removed, don't emit the event. + const isEmptyAccountAssetListUpdatedPayload = Object.values( + assetListUpdatedPayload, + ) + .map((item) => item.added.length + item.removed.length) + .every((item) => item === 0); + + if (!isEmptyAccountAssetListUpdatedPayload) { + await emitSnapKeyringEvent(snap, KeyringEvent.AccountAssetListUpdated, { + assets: assetListUpdatedPayload, + }); + } + + // Emit synthetic zero-balance entries for disappeared assets so clients can + // clear cached balances even when the backend omits zero-balance tokens + // instead of returning them explicitly. + const removedAssetsWithZeroBalance = disappearedAssets + .filter(shouldEmitAsset) + .map((asset) => ({ + ...asset, + rawAmount: '0', + uiAmount: '0', + })); + + const assetsToSave = [...assets, ...removedAssetsWithZeroBalance]; + // Save assets using repository + await this.#assetsRepository.saveMany(assetsToSave); + + // Broadcast the current snapshot plus synthetic zero-balance removals so the + // client can reconcile both visible assets and cached balances in one pass. + const balancesUpdatedPayload = [ + ...assets.filter(shouldEmitAsset), + ...removedAssetsWithZeroBalance, + ].reduce( + (acc, asset) => ({ + ...acc, + [asset.keyringAccountId]: { + ...(acc[asset.keyringAccountId] ?? {}), + [asset.assetType]: { + unit: asset.symbol, + amount: asset.uiAmount, + }, + }, + }), + {}, + ); + + // Traverse the balancesUpdatedPayload object to check if we have at least 1 account that has at least 1 balance updated. + const isSomeBalanceChanged = Object.values(balancesUpdatedPayload) + .map((accountAssets) => Object.keys(accountAssets).length) // To each accountAssets object, map the number of assetTypes + .some((count) => count > 0); + + // Only emit the event if some balance was changed. + if (isSomeBalanceChanged) { + await emitSnapKeyringEvent(snap, KeyringEvent.AccountBalancesUpdated, { + balances: balancesUpdatedPayload, + }); + } + } + + async getAll(): Promise { + const assetsByAccount = + (await this.#state.getKey('assets')) ?? + {}; + + return Object.values(assetsByAccount).flat(); + } + + /** + * Creates an asset entity with zero balance from a known CAIP-19 asset ID. + * Uses pre-calculated metadata from TokenMetadata. + * + * @param assetId - The CAIP-19 asset ID (e.g., KnownCaip19Id.TrxMainnet). + * @param keyringAccountId - The keyring account ID. + * @returns The asset entity with zero balance. + */ + #createZeroBalanceAsset( + assetId: KnownCaip19Id, + keyringAccountId: string, + ): AssetEntity { + const metadata = TokenMetadata[assetId as keyof typeof TokenMetadata]; + const { chainId } = parseCaipAssetType(assetId); + + return { + assetType: metadata.id, + keyringAccountId, + network: chainId as Network, + symbol: metadata.symbol, + decimals: metadata.decimals, + rawAmount: '0', + uiAmount: '0', + } as AssetEntity; + } + + async getAccountAssetsByScope( + scope: Network, + keyringAccountId: string, + ): Promise { + const savedAssets = + await this.#assetsRepository.getByAccountId(keyringAccountId); + + const visibleSavedAssets = savedAssets.filter( + (asset) => asset.network === scope, + ); + + const missingEssentialAssets: AssetEntity[] = []; + + for (const essentialAssetId of ESSENTIAL_ASSETS) { + const { chainId } = parseCaipAssetType(essentialAssetId as CaipAssetType); + + if ((chainId as Network) !== scope) { + continue; + } + + const savedAsset = savedAssets.find( + (asset) => (asset.assetType as string) === essentialAssetId, + ); + + if (!savedAsset) { + const zeroBalanceAsset = this.#createZeroBalanceAsset( + essentialAssetId as KnownCaip19Id, + keyringAccountId, + ); + missingEssentialAssets.push(zeroBalanceAsset); + } + } + + return [...visibleSavedAssets, ...missingEssentialAssets]; + } + + /** + * Extracts the ISO 4217 currency code (aka fiat ticker) from a fiat CAIP-19 asset type. + * + * @param caipAssetType - The CAIP-19 asset type. + * @returns The fiat ticker. + */ + #extractFiatTicker(caipAssetType: CaipAssetType): FiatTicker { + if (!SnapAssetsAdapter.isFiat(caipAssetType)) { + throw new Error('Passed caipAssetType is not a fiat asset'); + } + + const fiatTicker = + parseCaipAssetType(caipAssetType).assetReference.toLowerCase(); + + return fiatTicker as FiatTicker; + } + + /** + * Fetches fiat exchange rates and crypto prices for the given assets. + * This is shared logic between getMultipleTokenConversions and getMultipleTokensMarketData. + * + * @param allAssets - Array of all CAIP asset types (both fiat and crypto). + * @returns Promise resolving to fiat exchange rates and crypto prices. + */ + async #fetchPriceData(allAssets: CaipAssetType[]): Promise<{ + fiatExchangeRates: Record; + cryptoPrices: Record; + }> { + const cryptoAssets = allAssets.filter( + (asset) => !SnapAssetsAdapter.isFiat(asset), + ); + + const [fiatExchangeRates, cryptoPrices] = await Promise.all([ + this.#priceApiClient.getFiatExchangeRates(), + this.#priceApiClient.getMultipleSpotPrices(cryptoAssets, 'usd'), + ]); + + return { fiatExchangeRates, cryptoPrices }; + } + + /** + * Get the token conversions for a list of asset pairs. + * It caches the results for 1 hour. + * + * Beware: Inside we are using the Price API's `getFiatExchangeRates` method for fiat prices, + * `getMultipleSpotPrices` for crypto prices and then using USD as an intermediate currency + * to convert the prices to the correct currency. This is not entirely accurate but it's the + * best we can do with the current API. + * + * @param conversions - The asset pairs to get the conversions for. + * @returns The token conversions. + */ + async getMultipleTokenConversions( + conversions: { from: CaipAssetType; to: CaipAssetType }[], + ): Promise< + Record> + > { + if (conversions.length === 0) { + return {}; + } + + /** + * `from` and `to` can represent both fiat and crypto assets. For us to get their values + * the best approach is to use Price API's `getFiatExchangeRates` method for fiat prices, + * `getMultipleSpotPrices` for crypto prices and then using USD as an intermediate currency + * to convert the prices to the correct currency. + */ + const allAssets = conversions.flatMap((conversion) => [ + conversion.from, + conversion.to, + ]); + + const { fiatExchangeRates, cryptoPrices } = + await this.#fetchPriceData(allAssets); + + /** + * Now that we have the data, convert the `from`s to `to`s. + * + * We need to handle the following cases: + * 1. `from` and `to` are both fiat + * 2. `from` and `to` are both crypto + * 3. `from` is fiat and `to` is crypto + * 4. `from` is crypto and `to` is fiat + * + * We also need to keep in mind that although `cryptoPrices` are indexed + * by CAIP 19 IDs, the `fiatExchangeRates` are indexed by currency symbols. + * To convert fiat currency symbols to CAIP 19 IDs, we can use the + * `this.#fiatSymbolToCaip19Id` method. + */ + + const result: Record< + CaipAssetType, + Record + > = {}; + + conversions.forEach((conversion) => { + const { from, to } = conversion; + + result[from] ??= {}; + + let fromUsdRate: BigNumber; + let toUsdRate: BigNumber; + + if (SnapAssetsAdapter.isFiat(from)) { + /** + * Beware: + * We need to invert the fiat exchange rate because exchange rate != spot price + */ + const fiatExchangeRate = + fiatExchangeRates[this.#extractFiatTicker(from)]?.value; + + if (!fiatExchangeRate) { + result[from][to] = null; + return; + } + + fromUsdRate = new BigNumber(1).dividedBy(fiatExchangeRate); + } else { + fromUsdRate = new BigNumber(cryptoPrices[from]?.price ?? 0); + } + + if (SnapAssetsAdapter.isFiat(to)) { + /** + * Beware: + * We need to invert the fiat exchange rate because exchange rate != spot price + */ + const fiatExchangeRate = + fiatExchangeRates[this.#extractFiatTicker(to)]?.value; + + if (!fiatExchangeRate) { + result[from][to] = null; + return; + } + + toUsdRate = new BigNumber(1).dividedBy(fiatExchangeRate); + } else { + toUsdRate = new BigNumber(cryptoPrices[to]?.price ?? 0); + } + + if (fromUsdRate.isZero() || toUsdRate.isZero()) { + result[from][to] = null; + return; + } + + const rate = fromUsdRate.dividedBy(toUsdRate).toString(); + + const now = Date.now(); + + result[from][to] = { + rate, + conversionTime: now, + expirationTime: now + this.cacheTtlsMilliseconds.historicalPrices, + }; + }); + + return result; + } + + /** + * Computes the market data object in the target currency. + * + * @param spotPrice - The spot price of the asset in source currency. + * @param rate - The rate to convert the market data to from source currency to target currency. + * @returns The market data in the target currency. + */ + #computeMarketData( + spotPrice: SpotPrice, + rate: BigNumber, + ): FungibleAssetMarketData { + const marketDataInUsd = pick(spotPrice, [ + 'marketCap', + 'totalVolume', + 'circulatingSupply', + 'allTimeHigh', + 'allTimeLow', + 'pricePercentChange1h', + 'pricePercentChange1d', + 'pricePercentChange7d', + 'pricePercentChange14d', + 'pricePercentChange30d', + 'pricePercentChange200d', + 'pricePercentChange1y', + ]); + + const toCurrency = (value: number | null | undefined): string => { + return value === null || value === undefined + ? '' + : new BigNumber(value).dividedBy(rate).toString(); + }; + + const includeIfDefined = ( + key: string, + value: number | null | undefined, + ): Record => { + return value === null || value === undefined ? {} : { [key]: value }; + }; + + // Variations in percent don't need to be converted, they are independent of the currency + const pricePercentChange = { + ...includeIfDefined('PT1H', marketDataInUsd.pricePercentChange1h), + ...includeIfDefined('P1D', marketDataInUsd.pricePercentChange1d), + ...includeIfDefined('P7D', marketDataInUsd.pricePercentChange7d), + ...includeIfDefined('P14D', marketDataInUsd.pricePercentChange14d), + ...includeIfDefined('P30D', marketDataInUsd.pricePercentChange30d), + ...includeIfDefined('P200D', marketDataInUsd.pricePercentChange200d), + ...includeIfDefined('P1Y', marketDataInUsd.pricePercentChange1y), + }; + + const marketDataInToCurrency = { + fungible: true, + marketCap: toCurrency(marketDataInUsd.marketCap), + totalVolume: toCurrency(marketDataInUsd.totalVolume), + circulatingSupply: (marketDataInUsd.circulatingSupply ?? 0).toString(), // Circulating supply counts the number of tokens in circulation, so we don't convert + allTimeHigh: toCurrency(marketDataInUsd.allTimeHigh), + allTimeLow: toCurrency(marketDataInUsd.allTimeLow), + // Add pricePercentChange field only if it has values + ...(Object.keys(pricePercentChange).length > 0 + ? { pricePercentChange } + : {}), + } as FungibleAssetMarketData; + + return marketDataInToCurrency; + } + + async getMultipleTokensMarketData( + assets: { + asset: CaipAssetType; + unit: CaipAssetType; + }[], + ): Promise< + Record> + > { + if (assets.length === 0) { + return {}; + } + + /** + * `asset` and `unit` can represent both fiat and crypto assets. For us to get their values + * the best approach is to use Price API's `getFiatExchangeRates` method for fiat prices, + * `getMultipleSpotPrices` for crypto prices and then using USD as an intermediate currency + * to convert the prices to the correct currency. + */ + const allAssets = assets.flatMap((asset) => [asset.asset, asset.unit]); + + const { fiatExchangeRates, cryptoPrices } = + await this.#fetchPriceData(allAssets); + + const result: Record< + CaipAssetType, + Record + > = {}; + + assets.forEach((asset) => { + const { asset: assetType, unit } = asset; + + // Skip if we don't have price data for the asset + if (!cryptoPrices[assetType]) { + return; + } + + let unitUsdRate: BigNumber; + + if (SnapAssetsAdapter.isFiat(unit)) { + /** + * Beware: + * We need to invert the fiat exchange rate because exchange rate != spot price + */ + const fiatExchangeRate = + fiatExchangeRates[this.#extractFiatTicker(unit)]?.value; + + if (!fiatExchangeRate) { + return; + } + + unitUsdRate = new BigNumber(1).dividedBy(fiatExchangeRate); + } else { + unitUsdRate = new BigNumber(cryptoPrices[unit]?.price ?? 0); + } + + if (unitUsdRate.isZero()) { + return; + } + + // Initialize the nested structure for the asset if it doesn't exist + result[assetType] ??= {}; + + // Store the market data with the unit as the key + result[assetType][unit] = this.#computeMarketData( + cryptoPrices[assetType], + unitUsdRate, + ); + }); + + return result; + } + + /** + * Get historical prices for a token pair by calling the Price API. + * Similar to the Solana snap implementation. + * + * @param from - The asset to get historical prices for. + * @param to - The currency to convert prices to. + * @returns Historical price data with intervals. + */ + async getHistoricalPrice( + from: CaipAssetType, + to: CaipAssetType, + ): Promise<{ + intervals: HistoricalPriceIntervals; + updateTime: number; + expirationTime?: number; + }> { + assert(from, CaipAssetTypeStruct); + assert(to, CaipAssetTypeStruct); + + const toTicker = parseCaipAssetType(to).assetReference.toLowerCase(); + assert(toTicker, VsCurrencyParamStruct); + + const timePeriodsToFetch = ['1d', '7d', '1m', '3m', '1y', '1000y']; + + // For each time period, call the Price API to fetch the historical prices + const promises = timePeriodsToFetch.map(async (timePeriod) => + this.#priceApiClient + .getHistoricalPrices({ + assetType: from, + timePeriod, + vsCurrency: toTicker, + }) + // Wrap the response in an object with the time period and the response for easier reducing + .then((response) => ({ + timePeriod, + response, + })) + // Gracefully handle individual errors to avoid breaking the entire operation + .catch(async (error) => { + await this.#snapClient.trackError(error as Error); + this.#logger.warn( + `Error fetching historical prices for ${from} to ${to} with time period ${timePeriod}. Returning null object.`, + error, + ); + return { + timePeriod, + response: GET_HISTORICAL_PRICES_RESPONSE_NULL_OBJECT, + }; + }), + ); + + const wrappedHistoricalPrices = await Promise.all(promises); + + const intervals = wrappedHistoricalPrices.reduce( + (acc, { timePeriod, response }) => { + const iso8601Interval = `P${timePeriod.toUpperCase()}`; + acc[iso8601Interval] = response.prices.map((price) => [ + price[0], + price[1].toString(), + ]); + return acc; + }, + {}, + ); + + const now = Date.now(); + + const result = { + intervals, + updateTime: now, + expirationTime: now + this.cacheTtlsMilliseconds.historicalPrices, + }; + + return result; + } +} diff --git a/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.test.ts b/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.test.ts deleted file mode 100644 index 0e34e202..00000000 --- a/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { - SNAPS_ASSETS_MIGRATION_FLAG_KEYS, - SnapsAssetsMigrationStage, -} from '@metamask/assets-controller'; - -import { Network } from '../../constants'; -import type { CoreMessengerCaller } from '../../types/core-messenger'; - -import { - modeFromStage, - parseStageFromEnv, - parseStageFromRemoteFlags, - resolveAssetsMigrationMode, -} from './assetsMigrationMode'; - -const TRON_CHAIN_ID = Network.Mainnet; -const TRON_FLAG_KEY = SNAPS_ASSETS_MIGRATION_FLAG_KEYS.tron; - -describe('assetsMigrationMode', () => { - describe('modeFromStage', () => { - it('maps Off to snap', () => { - expect(modeFromStage(SnapsAssetsMigrationStage.Off)).toBe('snap'); - }); - - it.each([ - SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback, - SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - SnapsAssetsMigrationStage.ReadAssetsControllerOnly, - ])('maps active stage %s to controller', (stage) => { - expect(modeFromStage(stage)).toBe('controller'); - }); - }); - - describe('parseStageFromRemoteFlags', () => { - it('returns Off when remote flag is Off', () => { - expect( - parseStageFromRemoteFlags( - { [TRON_FLAG_KEY]: { stage: SnapsAssetsMigrationStage.Off } }, - TRON_CHAIN_ID, - ), - ).toBe(SnapsAssetsMigrationStage.Off); - }); - - it.each([ - SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback, - SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - SnapsAssetsMigrationStage.ReadAssetsControllerOnly, - ])('returns stage %s from remote flags', (stage) => { - expect( - parseStageFromRemoteFlags( - { [TRON_FLAG_KEY]: { stage } }, - TRON_CHAIN_ID, - ), - ).toBe(stage); - }); - - it('returns undefined when remote flags are missing', () => { - expect(parseStageFromRemoteFlags(undefined, TRON_CHAIN_ID)).toBeUndefined(); - expect(parseStageFromRemoteFlags({}, TRON_CHAIN_ID)).toBeUndefined(); - }); - }); - - describe('parseStageFromEnv', () => { - it.each(['off', '0'])('accepts Off alias %s', (value) => { - expect(parseStageFromEnv(value)).toBe(SnapsAssetsMigrationStage.Off); - }); - - it.each(['read-assets-controller-without-fallback', '2', 'controller'])( - 'accepts controller alias %s', - (value) => { - expect(parseStageFromEnv(value)).toBe( - SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - ); - }, - ); - - it.each([ - '1', - 'read-assets-controller-with-fallback', - '3', - 'read-assets-controller-only', - 'only', - 'with-fallback', - 'invalid', - ])('rejects unsupported env value %s', (value) => { - expect(parseStageFromEnv(value)).toBeUndefined(); - }); - }); - - describe('resolveAssetsMigrationMode', () => { - const createMessenger = ( - remoteFeatureFlags: Record | undefined, - ): CoreMessengerCaller => ({ - call: jest.fn().mockResolvedValue({ remoteFeatureFlags }), - }); - - it('prefers remote flags over env', async () => { - const coreMessenger = createMessenger({ - [TRON_FLAG_KEY]: { - stage: SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - }, - }); - - await expect( - resolveAssetsMigrationMode({ - coreMessenger, - chainId: TRON_CHAIN_ID, - isDev: true, - envStage: SnapsAssetsMigrationStage.Off, - }), - ).resolves.toBe('controller'); - }); - - it('uses env in non-production when remote flags are absent', async () => { - const coreMessenger = createMessenger({}); - - await expect( - resolveAssetsMigrationMode({ - coreMessenger, - chainId: TRON_CHAIN_ID, - isDev: true, - envStage: - SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - }), - ).resolves.toBe('controller'); - - await expect( - resolveAssetsMigrationMode({ - coreMessenger, - chainId: TRON_CHAIN_ID, - isDev: true, - envStage: SnapsAssetsMigrationStage.Off, - }), - ).resolves.toBe('snap'); - }); - - it('ignores env in production', async () => { - const coreMessenger = createMessenger({}); - - await expect( - resolveAssetsMigrationMode({ - coreMessenger, - chainId: TRON_CHAIN_ID, - isDev: false, - envStage: - SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - }), - ).resolves.toBe('snap'); - }); - - it('defaults to snap when everything is missing', async () => { - const coreMessenger = createMessenger(undefined); - - await expect( - resolveAssetsMigrationMode({ - coreMessenger, - chainId: TRON_CHAIN_ID, - isDev: true, - envStage: undefined, - }), - ).resolves.toBe('snap'); - }); - }); -}); diff --git a/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.ts b/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.ts deleted file mode 100644 index accefc64..00000000 --- a/packages/tron-wallet-snap/src/services/assets/assetsMigrationMode.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - SNAPS_ASSETS_MIGRATION_FLAG_KEYS, - SnapsAssetsMigrationStage, - getSnapsAssetsMigrationNamespace, - parseSnapsAssetsMigrationStage, -} from '@metamask/assets-controller'; -import type { Json } from '@metamask/utils'; - -import { Network } from '../../constants'; -import type { CoreMessengerCaller } from '../../types/core-messenger'; - -export type AssetsMigrationMode = 'snap' | 'controller'; - -const ENV_TO_STAGE: Record = { - off: SnapsAssetsMigrationStage.Off, - '0': SnapsAssetsMigrationStage.Off, - 'read-assets-controller-without-fallback': - SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - '2': SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - controller: SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, -}; - -/** - * Collapse Core stages into the two Snap outcomes. - * - * @param stage - Resolved migration stage. - * @returns `'snap'` when Off, otherwise `'controller'`. - */ -export function modeFromStage( - stage: SnapsAssetsMigrationStage, -): AssetsMigrationMode { - return stage === SnapsAssetsMigrationStage.Off ? 'snap' : 'controller'; -} - -/** - * Extract the migration stage for a chain from remote feature flags. - * - * @param remoteFeatureFlags - Remote feature flag payload from the wallet. - * @param chainId - CAIP-2 chain id for the network. - * @returns The configured stage, or `undefined` when the flag key is absent. - */ -export function parseStageFromRemoteFlags( - remoteFeatureFlags: Record | undefined, - chainId: string, -): SnapsAssetsMigrationStage | undefined { - if (!remoteFeatureFlags) { - return undefined; - } - - const namespace = getSnapsAssetsMigrationNamespace(chainId); - if (!namespace) { - return undefined; - } - - const flagKey = SNAPS_ASSETS_MIGRATION_FLAG_KEYS[namespace]; - if (!(flagKey in remoteFeatureFlags)) { - return undefined; - } - - return parseSnapsAssetsMigrationStage( - remoteFeatureFlags[flagKey] as Json | undefined, - ); -} - -/** - * Parse a raw `TRON_ASSETS_MIGRATION_STAGE` env value. - * - * Only Off and WithoutFallback aliases are accepted. WithFallback and Only - * strings are rejected so they cannot force controller mode without a remote flag. - * - * @param rawStage - Raw env value, possibly undefined. - * @returns The configured stage, or `undefined` when unset or invalid. - */ -export function parseStageFromEnv( - rawStage: string | undefined, -): SnapsAssetsMigrationStage | undefined { - const normalized = rawStage?.trim().toLowerCase(); - - if (normalized && normalized in ENV_TO_STAGE) { - return ENV_TO_STAGE[normalized]; - } - - return undefined; -} - -/** - * Resolve the assets migration mode for a network. - * - * Resolution order: - * 1. Remote feature flags - * 2. `TRON_ASSETS_MIGRATION_STAGE` in non-production builds - * 3. `'snap'` - * - * @param params - Resolution inputs. - * @param params.coreMessenger - Messenger used to read remote flags. - * @param params.chainId - CAIP-2 chain id for the network. - * @param params.isDev - Whether the Snap is running in a non-production build. - * @param params.envStage - Parsed env stage override. - * @returns The resolved migration mode. - */ -export async function resolveAssetsMigrationMode({ - coreMessenger, - chainId, - isDev, - envStage, -}: { - coreMessenger: CoreMessengerCaller; - chainId: string; - isDev: boolean; - envStage: SnapsAssetsMigrationStage | undefined; -}): Promise { - const state = await coreMessenger.call('RemoteFeatureFlagController:getState'); - const remoteStage = parseStageFromRemoteFlags( - state.remoteFeatureFlags, - chainId, - ); - - if (remoteStage !== undefined) { - return modeFromStage(remoteStage); - } - - if (isDev && envStage !== undefined) { - return modeFromStage(envStage); - } - - return 'snap'; -} - -export { SnapsAssetsMigrationStage }; diff --git a/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.ts b/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.ts index f90078d2..ea3359b1 100644 --- a/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.ts +++ b/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.ts @@ -234,11 +234,14 @@ export class ConfirmationHandler { account.address, ); - const [bandwidthAsset, energyAsset] = - await this.#assetsService.getAssetsByAccountId(account.id, [ + const bandwidthAndEnergyAssets = + await this.#assetsService.getAccountAssetsByIDs(account.id, [ Networks[scope].bandwidth.id, Networks[scope].energy.id, ]); + const bandwidthAsset = + bandwidthAndEnergyAssets[Networks[scope].bandwidth.id]; + const energyAsset = bandwidthAndEnergyAssets[Networks[scope].energy.id]; const availableEnergy = energyAsset ? new BigNumber(energyAsset.rawAmount) diff --git a/packages/tron-wallet-snap/src/services/send/SendService.ts b/packages/tron-wallet-snap/src/services/send/SendService.ts index 3e988e4f..dfdf73d5 100644 --- a/packages/tron-wallet-snap/src/services/send/SendService.ts +++ b/packages/tron-wallet-snap/src/services/send/SendService.ts @@ -111,13 +111,19 @@ export class SendService { /** * Get the user's current balances for the asset being sent and TRX (for fees). */ - const [assetBalance, nativeTokenAsset, bandwidthAsset, energyAsset] = - await this.#assetsService.getAssetsByAccountId(fromAccountId, [ + const assets = await this.#assetsService.getAccountAssetsByIDs( + fromAccountId, + [ asset.assetType, nativeTokenId, Networks[scope].bandwidth.id, Networks[scope].energy.id, - ]); + ], + ); + const assetBalance = assets[asset.assetType]; + const nativeTokenAsset = assets[nativeTokenId]; + const bandwidthAsset = assets[Networks[scope].bandwidth.id]; + const energyAsset = assets[Networks[scope].energy.id]; const assetToSendBalance = assetBalance ? new BigNumber(assetBalance.uiAmount) diff --git a/packages/tron-wallet-snap/src/types/core-messenger.ts b/packages/tron-wallet-snap/src/types/core-messenger.ts index b90518d2..67073282 100644 --- a/packages/tron-wallet-snap/src/types/core-messenger.ts +++ b/packages/tron-wallet-snap/src/types/core-messenger.ts @@ -1,12 +1,19 @@ -import type { AssetsControllerGetAssetAction } from '@metamask/assets-controller'; +import type { + AssetsControllerGetAssetAction, + AssetsControllerGetAssetsAction, +} from '@metamask/assets-controller'; import type { Messenger } from '@metamask/messenger'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; import type { AsyncMessenger } from '@metamask/snaps-sdk'; -export type { AssetsControllerGetAssetAction }; +export type { + AssetsControllerGetAssetAction, + AssetsControllerGetAssetsAction, +}; export type CoreMessengerActions = | AssetsControllerGetAssetAction + | AssetsControllerGetAssetsAction | RemoteFeatureFlagControllerGetStateAction; /** diff --git a/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx b/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx index 8340ce50..70f45b4a 100644 --- a/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx +++ b/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx @@ -93,7 +93,7 @@ export async function render( // Parallelize: Get preferences + Fetch account assets const [preferences, accountAssets] = await Promise.all([ snapClient.getPreferences().catch(() => DEFAULT_CONTEXT.preferences), - assetsService.getAssetsByAccountId(account.id, [ + assetsService.getAccountAssetsByIDs(account.id, [ Networks[scope as Network].nativeToken.id, Networks[scope as Network].bandwidth.id, Networks[scope as Network].energy.id, @@ -107,7 +107,10 @@ export async function render( // Calculate fees try { - const [nativeTokenAsset, bandwidthAsset, energyAsset] = accountAssets; + const nativeTokenAsset = + accountAssets[Networks[scope as Network].nativeToken.id]; + const bandwidthAsset = accountAssets[Networks[scope as Network].bandwidth.id]; + const energyAsset = accountAssets[Networks[scope as Network].energy.id]; const availableEnergy = energyAsset ? new BigNumber(energyAsset.rawAmount)