Skip to content

Commit aa96e24

Browse files
muhammadausclaude
andcommitted
Add EIP-712 test suite, remote metadata service, verification badges, and popup wallet mode
- Add RemoteMetadataService for fetching metadata from KaiSign API with retry on 429 rate limiting, verification data attachment, and EIP-712 cache isolation to prevent calldata fixture cache pollution - Add verification badges to test output ([Verified]/[Unverified]/[Mismatch]) matching the popup's on-chain attestation display logic - Add EIP-712 CoW Protocol Order test suite (5 tests: sell, buy, partial fill, unknown type, Gnosis Chain) with --remote flag support - Add wallet mode toggle to popup UI with MetaMask Enhancer mode - Clean up manifest.json: remove unused script references - Update CoW GPv2 Order fixture to match production API format 71/71 tests passing locally, 71/71 with --remote Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ab408c9 commit aa96e24

11 files changed

Lines changed: 743 additions & 28 deletions

File tree

manifest.json

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33
"name": "KaiSign",
44
"version": "1.3.0",
5-
"description": "Transaction analysis and clear signing for Ethereum wallets - Direct Ledger support",
5+
"description": "Transaction analysis and clear signing for Ethereum wallets",
66
"icons": {
77
"16": "icons/icon-16.png",
88
"32": "icons/icon-32.png",
@@ -52,10 +52,6 @@
5252
{
5353
"matches": ["<all_urls>"],
5454
"js": [
55-
"rpc-provider.js",
56-
"ledger-connection-service.js",
57-
"eip1193-provider.js",
58-
"kaisign-provider.js",
5955
"name-resolution-service.js",
6056
"subgraph-metadata.js",
6157
"onchain-verifier.js",
@@ -74,10 +70,6 @@
7470
"web_accessible_resources": [
7571
{
7672
"resources": [
77-
"rpc-provider.js",
78-
"ledger-connection-service.js",
79-
"eip1193-provider.js",
80-
"kaisign-provider.js",
8173
"name-resolution-service.js",
8274
"subgraph-metadata.js",
8375
"onchain-verifier.js",

popup.css

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,96 @@ body.theme-dark::after {
194194
height: 16px;
195195
}
196196

197+
/* Mode Toggle Section */
198+
.mode-toggle-section {
199+
padding: 12px 16px;
200+
background: var(--surface);
201+
border-bottom: 1px solid var(--border);
202+
flex-shrink: 0;
203+
z-index: 1;
204+
}
205+
206+
.mode-toggle-header {
207+
display: flex;
208+
justify-content: space-between;
209+
align-items: center;
210+
margin-bottom: 8px;
211+
}
212+
213+
.mode-label {
214+
font-size: 11px;
215+
font-weight: 600;
216+
color: var(--text-muted);
217+
text-transform: uppercase;
218+
letter-spacing: 0.5px;
219+
}
220+
221+
.mode-status {
222+
font-size: 11px;
223+
color: var(--accent);
224+
font-weight: 500;
225+
}
226+
227+
.mode-buttons {
228+
display: flex;
229+
gap: 8px;
230+
}
231+
232+
.mode-btn {
233+
flex: 1;
234+
display: flex;
235+
align-items: center;
236+
justify-content: center;
237+
gap: 6px;
238+
padding: 8px 12px;
239+
background: var(--bg);
240+
border: 1px solid var(--border);
241+
border-radius: 8px;
242+
color: var(--text-muted);
243+
font-size: 11px;
244+
font-weight: 500;
245+
cursor: pointer;
246+
transition: all 0.2s;
247+
}
248+
249+
.mode-btn:hover {
250+
background: var(--surface-strong);
251+
color: var(--text);
252+
border-color: var(--accent);
253+
}
254+
255+
.mode-btn.active {
256+
background: linear-gradient(135deg, var(--accent), var(--accent-cool));
257+
color: white;
258+
border-color: transparent;
259+
box-shadow: 0 4px 12px rgba(15, 159, 154, 0.3);
260+
}
261+
262+
.mode-btn svg {
263+
flex-shrink: 0;
264+
}
265+
266+
.btn-small {
267+
padding: 4px 10px;
268+
background: var(--accent);
269+
color: white;
270+
border: none;
271+
border-radius: 4px;
272+
font-size: 10px;
273+
font-weight: 500;
274+
cursor: pointer;
275+
transition: all 0.2s;
276+
}
277+
278+
.btn-small:hover {
279+
background: var(--accent-strong);
280+
}
281+
282+
.btn-small:disabled {
283+
opacity: 0.5;
284+
cursor: not-allowed;
285+
}
286+
197287
/* Search */
198288
.search-wrapper {
199289
position: relative;

popup.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,23 @@
3030
</div>
3131
</header>
3232

33+
<!-- Wallet Mode Toggle -->
34+
<div class="mode-toggle-section" id="modeSection">
35+
<div class="mode-toggle-header">
36+
<span class="mode-label">Wallet Mode</span>
37+
<span class="mode-status" id="modeStatus">Loading...</span>
38+
</div>
39+
<div class="mode-buttons">
40+
<button class="mode-btn active" id="modeEnhancer" title="Intercept MetaMask transactions">
41+
<svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14">
42+
<path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 13A6 6 0 1 1 8 2a6 6 0 0 1 0 12z"/>
43+
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
44+
</svg>
45+
MetaMask Enhancer
46+
</button>
47+
</div>
48+
</div>
49+
3350
<!-- Search -->
3451
<div class="search-wrapper">
3552
<svg viewBox="0 0 16 16" fill="currentColor">

popup.js

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ let transactions = [];
66
let currentSearch = '';
77
let importData = null;
88
let activeDetailTx = null;
9+
let currentMode = 'enhancer';
910

1011
// DOM Elements
1112
const elements = {
@@ -36,16 +37,99 @@ const elements = {
3637
closeDetailModalBtn: document.getElementById('closeDetailModalBtn'),
3738
copyRawBtn: document.getElementById('copyRawBtn'),
3839
copyJsonBtn: document.getElementById('copyJsonBtn'),
39-
toast: document.getElementById('toast')
40+
toast: document.getElementById('toast'),
41+
// Mode toggle elements
42+
modeSection: document.getElementById('modeSection'),
43+
modeStatus: document.getElementById('modeStatus'),
44+
modeEnhancer: document.getElementById('modeEnhancer')
4045
};
4146

4247
// Initialize
4348
document.addEventListener('DOMContentLoaded', init);
4449

4550
async function init() {
4651
await loadTheme();
52+
await loadMode();
4753
await loadData();
4854
setupEventListeners();
55+
setupModeListeners();
56+
}
57+
58+
// Load wallet mode from storage
59+
async function loadMode() {
60+
try {
61+
const result = await chrome.storage.local.get(['kaisign_mode']);
62+
currentMode = result.kaisign_mode || 'enhancer';
63+
updateModeUI();
64+
} catch (error) {
65+
console.error('[KaiSign] Failed to load mode:', error);
66+
currentMode = 'enhancer';
67+
updateModeUI();
68+
}
69+
}
70+
71+
// Update mode UI based on current mode
72+
function updateModeUI() {
73+
// Update button states
74+
if (elements.modeEnhancer) {
75+
elements.modeEnhancer.classList.toggle('active', currentMode === 'enhancer');
76+
}
77+
78+
// Update status text
79+
if (elements.modeStatus) {
80+
elements.modeStatus.textContent = 'MetaMask Enhancer';
81+
elements.modeStatus.style.color = 'var(--accent)';
82+
}
83+
}
84+
85+
// Set wallet mode
86+
async function setMode(mode) {
87+
try {
88+
await chrome.storage.local.set({ kaisign_mode: mode });
89+
currentMode = mode;
90+
updateModeUI();
91+
92+
// Get RPC settings to sync along with mode
93+
const settings = await chrome.storage.local.get(['settings']);
94+
const rpcEndpoints = settings.settings?.rpcEndpoints || {};
95+
96+
// Sync to localStorage in the active tab so content scripts can read it
97+
// (Content scripts in MAIN world can't access chrome.storage)
98+
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
99+
if (tabs[0]) {
100+
chrome.scripting.executeScript({
101+
target: { tabId: tabs[0].id },
102+
func: (m, rpc) => {
103+
localStorage.setItem('kaisign_mode', m);
104+
localStorage.setItem('kaisign_rpc_endpoints', JSON.stringify(rpc));
105+
// Notify RPC provider to reload
106+
if (window.KaiSignRPC && window.KaiSignRPC.reloadEndpoints) {
107+
window.KaiSignRPC.reloadEndpoints();
108+
}
109+
},
110+
args: [mode, rpcEndpoints],
111+
world: 'MAIN'
112+
}).catch(() => {});
113+
114+
chrome.tabs.sendMessage(tabs[0].id, {
115+
type: 'KAISIGN_MODE_CHANGED',
116+
mode: mode
117+
}).catch(() => {});
118+
}
119+
});
120+
121+
showToast('MetaMask Enhancer mode active. Refresh page to apply.', 'success');
122+
} catch (error) {
123+
console.error('[KaiSign] Failed to set mode:', error);
124+
showToast('Failed to switch mode', 'error');
125+
}
126+
}
127+
128+
// Setup mode toggle listeners
129+
function setupModeListeners() {
130+
if (elements.modeEnhancer) {
131+
elements.modeEnhancer.addEventListener('click', () => setMode('enhancer'));
132+
}
49133
}
50134

51135
async function loadTheme() {

tests/fixtures/metadata/eip712/cow-gpv2-order.json

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"$schema": "../../erc7730-v1.schema.json",
23
"context": {
34
"eip712": {
45
"domain": {
@@ -7,6 +8,12 @@
78
"chainId": 1,
89
"verifyingContract": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
910
},
11+
"deployments": [
12+
{ "chainId": 1, "verifyingContract": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" },
13+
{ "chainId": 100, "verifyingContract": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" },
14+
{ "chainId": 42161, "verifyingContract": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" },
15+
{ "chainId": 8453, "verifyingContract": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" }
16+
],
1017
"primaryType": "Order",
1118
"types": {
1219
"EIP712Domain": [
@@ -29,28 +36,31 @@
2936
{ "name": "sellTokenBalance", "type": "string" },
3037
{ "name": "buyTokenBalance", "type": "string" }
3138
]
32-
},
33-
"name": "CoW Protocol Order",
34-
"description": "CoW Protocol (Gnosis Protocol v2) swap order"
39+
}
40+
}
41+
},
42+
"metadata": {
43+
"owner": "CoW Protocol",
44+
"info": {
45+
"url": "https://cow.fi",
46+
"legalName": "CoW Protocol"
3547
}
3648
},
3749
"display": {
3850
"formats": {
3951
"Order": {
4052
"intent": "Sign CoW Protocol order",
53+
"interpolatedIntent": "Swap {sellAmount} {sellToken} for {buyAmount} {buyToken}",
4154
"fields": [
42-
{ "path": "sellToken", "label": "Sell Token", "format": "address" },
43-
{ "path": "buyToken", "label": "Buy Token", "format": "address" },
55+
{ "path": "sellToken", "label": "Sell Token", "format": "addressName" },
56+
{ "path": "buyToken", "label": "Buy Token", "format": "addressName" },
4457
{ "path": "sellAmount", "label": "Sell Amount", "format": "amount", "params": { "tokenPath": "$.sellToken" } },
45-
{ "path": "buyAmount", "label": "Min Buy Amount", "format": "amount", "params": { "tokenPath": "$.buyToken" } },
46-
{ "path": "receiver", "label": "Receiver", "format": "address" },
58+
{ "path": "buyAmount", "label": "Min Receive", "format": "amount", "params": { "tokenPath": "$.buyToken" } },
59+
{ "path": "receiver", "label": "Receiver", "format": "addressName" },
4760
{ "path": "validTo", "label": "Valid Until", "format": "timestamp" },
48-
{ "path": "feeAmount", "label": "Fee Amount", "format": "amount", "params": { "tokenPath": "$.sellToken" } },
61+
{ "path": "feeAmount", "label": "Fee", "format": "amount", "params": { "tokenPath": "$.sellToken" } },
4962
{ "path": "kind", "label": "Order Type", "format": "string" },
50-
{ "path": "partiallyFillable", "label": "Partial Fill", "format": "boolean" },
51-
{ "path": "sellTokenBalance", "label": "Sell Token Source", "format": "string" },
52-
{ "path": "buyTokenBalance", "label": "Buy Token Dest", "format": "string" },
53-
{ "path": "appData", "label": "App Data", "format": "hex" }
63+
{ "path": "partiallyFillable", "label": "Partial Fill", "format": "boolean" }
5464
]
5565
}
5666
}

0 commit comments

Comments
 (0)