// ==UserScript==
// @name MSU 掛幣檢測器
// @namespace http://tampermonkey.net/
// @version 0.2
// @author Alex from MyGOTW
// @description 查詢是否買幣
// @match https://msu.io/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'msu_activity_data';
const MAX_PAGES = 10;
function weiToEth(wei) {
const eth = Number(wei) / Math.pow(10, 18);
if (!eth || eth < 0.0001) {
return NaN;
}
return parseFloat(eth.toFixed(4));
}
async function initialize() {
console.log('Initialize being called with URL:', window.location.href);
const matchUrl = /\?tab=activity(?:&page=\d+)?$/;
if (!window.location.href.includes('/marketplace/inventory/')) {
return;
}
else if(matchUrl.test(window.location.search) || window.location.search.includes('activityType=ActivityType_Buy') || window.location.search.includes('activityType=ActivityType_Sell')){
try {
console.log('啟動掛幣檢測器');
// 先顯示讀取中 UI
createLoadingUI();
const data = await getHistoryData();
if (data && data.length > 0) {
const suspiciousActivities = filterSuspiciousActivities(data);
console.log('可疑交易清單:', suspiciousActivities);
console.log(`找到 ${suspiciousActivities.length} 筆可疑交易`);
// 移除讀取中 UI,顯示結果
const loadingElement = document.getElementById('msu-notification');
if (loadingElement) {
loadingElement.remove();
}
createNotificationUI(suspiciousActivities);
}
} catch (error) {
console.error('getHistoryData error:', error);
// 發生錯誤時也要移除讀取中 UI
const loadingElement = document.getElementById('msu-notification');
if (loadingElement) {
loadingElement.remove();
}
// 顯示錯誤通知
createErrorUI();
}
}
}
const getHistoryData = async () => {
try {
const pathName = window.location.pathname;
const walletAddress = pathName.split('/')[3];
console.log('walletAddress:', walletAddress)
if(walletAddress){
// 檢查 localStorage 中的錢包地址是否相同
const storedData = getStoredData();
if(storedData?.walletAddress !== walletAddress) {
clearStoredData();
}
if(storedData?.data && storedData.walletAddress === walletAddress) {
console.log('使用已儲存的資料:', storedData.data);
// 即使使用快取資料,也稍微延遲一下以顯示讀取動畫
await new Promise(resolve => setTimeout(resolve, 500));
return storedData.data;
}
let fullData = [];
const firstData = await fetchData(walletAddress, 1);
if(!firstData) return;
fullData.push(firstData);
const totalPages = Math.min(
Math.ceil(firstData.paginationResult.totalCount / firstData.paginationResult.pageSize),
MAX_PAGES
);
for(let i = 2; i <= totalPages; i++){
const data = await fetchData(walletAddress, i);
if(data) {
fullData.push(data);
}
await new Promise(resolve => setTimeout(resolve, 50));
}
saveData(walletAddress, fullData);
console.log('完整資料:', fullData);
return fullData;
}
} catch (error) {
console.error('getHistoryData error:', error);
throw error; // 向上拋出錯誤以觸發錯誤處理
}
}
// localStorage 相關函數
function getStoredData() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('讀取儲存資料錯誤:', error);
return null;
}
}
function saveData(walletAddress, data) {
try {
const saveObj = {
walletAddress,
data,
timestamp: Date.now()
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(saveObj));
} catch (error) {
console.error('儲存資料錯誤:', error);
}
}
function clearStoredData() {
localStorage.removeItem(STORAGE_KEY);
}
const fetchData = async (walletAddress, pageNo = 1) => {
try {
const response = await fetch(`https://msu.io/marketplace/api/marketplace/inventory/${walletAddress}/activities?activityType=ActivityType_All&tokenType=items&paginationParam.pageNo=${pageNo}&paginationParam.pageSize=30`, {
"headers": {
"accept": "*/*",
"accept-language": "zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/json",
"priority": "u=1, i"
},
"referrer": `https://msu.io/marketplace/inventory/${walletAddress}?tab=activity`,
"referrerPolicy": "origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
});
const data = await response.json();
return data;
} catch (error) {
console.error('fetchData error:', error);
return null;
}
}
function filterSuspiciousActivities(fullData) {
try {
// 整合所有頁面的 activities,並保留頁數資訊
const allActivities = fullData.reduce((acc, page) => {
const activitiesWithPage = page.activities.map(activity => ({
...activity,
pageInfo: {
pageNo: page.paginationResult.currPageNo,
totalCount: page.paginationResult.totalCount,
pageSize: page.paginationResult.pageSize,
isLastPage: page.paginationResult.isLastPage
}
}));
return acc.concat(activitiesWithPage);
}, []);
// 篩選可疑交易
const suspiciousActivities = allActivities.filter(activity => {
const ethPrice = weiToEth(activity.priceWei);
return activity.activityType === "ActivityType_Sell" && ethPrice >= 100;
});
// 保留完整資料並格式化
const formattedActivities = suspiciousActivities.map(activity => ({
// 物品資訊
tokenId: activity.tokenId,
name: activity.name,
imageUrl: activity.imageUrl,
mintingNo: activity.mintingNo,
potentialGrade: activity.potentialGrade,
// 交易資訊
price: {
wei: activity.priceWei,
eth: weiToEth(activity.priceWei)
},
quantity: activity.quantity,
activityType: activity.activityType,
// 賣家資訊
seller: {
address: activity.walletAddrFrom,
nickname: activity.nicknameFrom,
profileUrl: `https://msu.io/marketplace/inventory/${activity.walletAddrFrom}`
},
// 買家資訊
buyer: {
address: activity.walletAddrTo,
nickname: activity.nicknameTo,
profileUrl: `https://msu.io/marketplace/inventory/${activity.walletAddrTo}`
},
// 時間資訊
time: {
original: activity.createdAt,
formatted: new Date(activity.createdAt).toLocaleString('zh-TW')
},
// 物品連結
itemUrl: `https://msu.io/marketplace/nft/${activity.tokenId}`,
// 分頁資訊
pageInfo: activity.pageInfo,
activityUrl: `https://msu.io/marketplace/inventory/${activity.walletAddrFrom}?tab=activity&page=${activity.pageInfo.pageNo}`
}));
console.log('可疑交易清單:', formattedActivities);
console.log(`找到 ${formattedActivities.length} 筆可疑交易`);
// 建立通知 UI
createNotificationUI(formattedActivities);
return formattedActivities;
} catch (error) {
console.error('篩選可疑交易時發生錯誤:', error);
return [];
}
}
function createNotificationUI(suspiciousActivities) {
// 檢查是否已存在通知 UI
if (document.getElementById('msu-notification')) {
return;
}
// 創建樣式
const style = document.createElement('style');
style.textContent = `
#msu-notification {
position: fixed;
left: 20px;
bottom: 20px;
z-index: 9999;
font-family: Arial, sans-serif;
}
.notification-btn {
padding: 10px 20px;
border-radius: 5px;
border: none;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
}
.normal-btn {
background-color: #4CAF50;
color: white;
}
.warning-btn {
background-color: #ff4444;
color: white;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.detail-window {
display: none;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: #1b1b1b;
color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
max-width: 80vw;
max-height: 80vh;
overflow-y: auto;
z-index: 10000;
}
.detail-window h2 {
color: white;
margin-bottom: 20px;
}
.detail-window.show {
display: block;
}
.close-btn {
position: absolute;
right: 10px;
top: 10px;
cursor: pointer;
font-size: 20px;
color: #ffffff;
}
.suspicious-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
color: white;
}
.suspicious-table th, .suspicious-table td {
padding: 10px;
border: 1px solid #333;
text-align: left;
}
.suspicious-table th {
background-color: #2d2d2d;
color: white;
}
.suspicious-table tr {
background-color: #1b1b1b;
}
.suspicious-table tr:hover {
background-color: #2d2d2d;
}
.item-image {
width: 50px;
height: 50px;
object-fit: contain;
border-radius: 5px;
}
.address-link {
color: #66b3ff;
text-decoration: none;
}
.address-link:hover {
text-decoration: underline;
color: #99ccff;
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9999;
}
.overlay.show {
display: block;
}
/* 捲軸樣式 */
.detail-window::-webkit-scrollbar {
width: 8px;
}
.detail-window::-webkit-scrollbar-track {
background: #2d2d2d;
border-radius: 4px;
}
.detail-window::-webkit-scrollbar-thumb {
background: #4d4d4d;
border-radius: 4px;
}
.detail-window::-webkit-scrollbar-thumb:hover {
background: #666;
}
/* 一般連結樣式 */
.detail-window a {
color: #66b3ff;
text-decoration: none;
}
.detail-window a:hover {
color: #99ccff;
text-decoration: underline;
}
`;
document.head.appendChild(style);
// 創建通知按鈕和詳細資訊視窗
const notificationDiv = document.createElement('div');
notificationDiv.id = 'msu-notification';
const buttonText = suspiciousActivities.length > 0
? `疑似買幣 (${suspiciousActivities.length})`
: '帳戶正常';
const buttonClass = suspiciousActivities.length > 0
? 'warning-btn'
: 'normal-btn';
notificationDiv.innerHTML = `
<button class="notification-btn ${buttonClass}">${buttonText}</button>
<div class="overlay"></div>
<div class="detail-window">
<span class="close-btn">×</span>
<h2>可疑交易清單</h2>
<table class="suspicious-table">
<thead>
<tr>
<th>物品</th>
<th>名稱</th>
<th>價格</th>
<th>賣家</th>
<th>買家</th>
<th>時間</th>
<th>頁數</th>
</tr>
</thead>
<tbody>
${suspiciousActivities.map(activity => `
<tr>
<td>
<a href="${activity.itemUrl}" target="_blank">
<img src="${activity.imageUrl}" class="item-image" alt="${activity.name}">
</a>
</td>
<td>
<a href="${activity.itemUrl}" target="_blank">
${activity.name} #${activity.mintingNo}
</a>
</td>
<td>${activity.price.eth} ETH</td>
<td>
<a href="${activity.seller.profileUrl}" target="_blank" class="address-link">
${activity.seller.nickname || '未知'}<br>
${activity.seller.address.slice(0, 6)}...${activity.seller.address.slice(-4)}
</a>
</td>
<td>
<a href="${activity.buyer.profileUrl}" target="_blank" class="address-link">
${activity.buyer.nickname || '未知'}<br>
${activity.buyer.address.slice(0, 6)}...${activity.buyer.address.slice(-4)}
</a>
</td>
<td>${activity.time.formatted}</td>
<td>
<a href="${activity.activityUrl}" target="_blank">
第 ${activity.pageInfo.pageNo} 頁
</a>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
document.body.appendChild(notificationDiv);
// 事件處理
const button = notificationDiv.querySelector('.notification-btn');
const detailWindow = notificationDiv.querySelector('.detail-window');
const overlay = notificationDiv.querySelector('.overlay');
const closeBtn = notificationDiv.querySelector('.close-btn');
button.addEventListener('click', () => {
if (suspiciousActivities.length > 0) {
detailWindow.classList.add('show');
overlay.classList.add('show');
}
});
closeBtn.addEventListener('click', () => {
detailWindow.classList.remove('show');
overlay.classList.remove('show');
});
overlay.addEventListener('click', () => {
detailWindow.classList.remove('show');
overlay.classList.remove('show');
});
}
// 先建立一個簡單的讀取中 UI
function createLoadingUI() {
// 檢查是否已存在
if (document.getElementById('msu-notification')) {
return;
}
const style = document.createElement('style');
style.textContent = `
.loading-btn {
background-color: #666 !important;
position: relative;
cursor: wait !important;
}
.loading-btn::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 50%;
right: 10px;
transform: translateY(-50%);
border: 2px solid #ffffff;
border-radius: 50%;
border-right-color: transparent;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from { transform: translateY(-50%) rotate(0deg); }
to { transform: translateY(-50%) rotate(360deg); }
}
`;
document.head.appendChild(style);
const notificationDiv = document.createElement('div');
notificationDiv.id = 'msu-notification';
notificationDiv.innerHTML = `
<button class="notification-btn loading-btn">資料讀取中...</button>
`;
document.body.appendChild(notificationDiv);
}
// 新增錯誤通知 UI
function createErrorUI() {
const notificationDiv = document.createElement('div');
notificationDiv.id = 'msu-notification';
notificationDiv.innerHTML = `
<button class="notification-btn" style="background-color: #ff4444;">
資料載入失敗
</button>
`;
document.body.appendChild(notificationDiv);
// 3秒後自動移除錯誤通知
setTimeout(() => {
const errorElement = document.getElementById('msu-notification');
if (errorElement) {
errorElement.remove();
}
}, 3000);
}
initialize();
// URL 變化監聽
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
handleUrlChange('pushState');
};
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
handleUrlChange('replaceState');
};
window.addEventListener('popstate', function () {
handleUrlChange('popstate');
});
function handleUrlChange(method) {
console.log(`小精靈通知: [${method}] URL 已變化: ${window.location.href}`);
initialize();
}
})();