solana-studio 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +59 -0
- data/app/assets/javascripts/solana_studio/redirect_provider.js +299 -0
- data/app/assets/javascripts/solana_studio/wallet_journal.js +154 -0
- data/app/assets/javascripts/solana_studio/wallet_ops.js +260 -0
- data/app/assets/javascripts/solana_studio/wallet_transport.js +367 -0
- data/app/views/solana_studio/modals/_wallet_connect.html.erb +94 -4
- data/app/views/solana_studio/modals/_web3_step_up.html.erb +44 -2
- data/lib/solana_studio/engine.rb +4 -0
- data/lib/solana_studio/version.rb +1 -1
- metadata +6 -2
|
@@ -76,6 +76,43 @@
|
|
|
76
76
|
browser, or to do something else entirely. The default
|
|
77
77
|
calls window.startPhantomDeepLink(linkMode, userId).
|
|
78
78
|
onBack() the Back button; DEFAULT closes the modal
|
|
79
|
+
|
|
80
|
+
CONTRACT WITH THE HOST'S JS. This gem ships no routes at all, and the JavaScript
|
|
81
|
+
it does ship provides no global in this list: solana_studio/network_guard.js
|
|
82
|
+
plus the redirect-transport primitives (wallet_transport, redirect_provider,
|
|
83
|
+
wallet_journal, wallet_ops). So every global below is the HOST'S to provide. Each is reached behind a `typeof` guard, and an absent
|
|
84
|
+
one degrades this card rather than breaking it — which is the whole reason the
|
|
85
|
+
list is written down here instead of being rediscovered per consumer.
|
|
86
|
+
|
|
87
|
+
window[connect_fn] REQUIRED. Connect + verify. Default
|
|
88
|
+
window.solanaConnectAndVerify.
|
|
89
|
+
window.walletProvider REQUIRED for the rows: .available() lists the
|
|
90
|
+
detected wallets this card paints.
|
|
91
|
+
parseSolanaError(msg) optional. Maps a wallet string to a sentence
|
|
92
|
+
a user can act on; absent means the wallet's
|
|
93
|
+
own words are shown unmapped.
|
|
94
|
+
window.handleSolanaVerifySuccess(result)
|
|
95
|
+
optional. Post-verify hook on the happy path.
|
|
96
|
+
window.startPhantomDeepLink(linkMode, userId)
|
|
97
|
+
optional. The mobile deep link; absent hides
|
|
98
|
+
the Phantom deep-link row entirely.
|
|
99
|
+
window.reportWalletFailure(stage, provider, raw, mapped)
|
|
100
|
+
optional. Fire-and-forget observation, called
|
|
101
|
+
ONCE per caught rejection with stage
|
|
102
|
+
'wallet_connect'. `raw` is what the wallet
|
|
103
|
+
said and `mapped` is what the user was shown;
|
|
104
|
+
a host that records only one half cannot
|
|
105
|
+
diagnose a MIS-mapping, which is the failure
|
|
106
|
+
this exists for. Return value is ignored and
|
|
107
|
+
a throw is swallowed — see the call site.
|
|
108
|
+
Absent means this surface is DARK: the user
|
|
109
|
+
is served correctly and nothing is recorded
|
|
110
|
+
anywhere. Reference implementation and the
|
|
111
|
+
receiving endpoint (both host-owned, because
|
|
112
|
+
they need an ErrorLog this gem cannot assume)
|
|
113
|
+
live in turf-monster:
|
|
114
|
+
app/javascript/solana_errors.js and
|
|
115
|
+
POST /auth/solana/report_failure.
|
|
79
116
|
%>
|
|
80
117
|
<%
|
|
81
118
|
store = local_assigns.fetch(:store, "modals")
|
|
@@ -158,9 +195,25 @@
|
|
|
158
195
|
// SECOND Phantom row pointing at a desktop download page the user
|
|
159
196
|
// cannot act on. ONLY when a deep link can replace it: with no deep
|
|
160
197
|
// link the install row is the only Phantom path there is, dead end on
|
|
161
|
-
// iOS or not, and removing it leaves the user nothing.
|
|
162
|
-
//
|
|
163
|
-
//
|
|
198
|
+
// iOS or not, and removing it leaves the user nothing.
|
|
199
|
+
//
|
|
200
|
+
// SOLFLARE AND BACKPACK STILL GET INSTALL ROWS HERE, and the reason is NOT
|
|
201
|
+
// the one this comment used to give. It claimed there was no deep link for
|
|
202
|
+
// them, which is FALSE and was costing those users: verified against both
|
|
203
|
+
// vendors' docs 2026-09-07, Solflare (solflare.com/ul/v1) and Backpack
|
|
204
|
+
// (backpack.app/ul/v1) each ship a full deeplink protocol, forked from
|
|
205
|
+
// Phantom's and sharing its encryption scheme and parameter names. So on a
|
|
206
|
+
// phone this row hands them a DESKTOP EXTENSION download page — a silent
|
|
207
|
+
// dead end, with no error, for a wallet that could have worked.
|
|
208
|
+
//
|
|
209
|
+
// What is actually missing is on OUR side: canDeepLink below asks whether
|
|
210
|
+
// ONE Phantom-specific global exists, so no other wallet can answer yes.
|
|
211
|
+
// solana_studio/redirect_provider.js now knows all three, and rewiring
|
|
212
|
+
// these three getters to ask it is the fix. Deliberately NOT done in the
|
|
213
|
+
// change that added that file: these getters are pinned by exact-source
|
|
214
|
+
// assertions in test/views/wallet_connect_picker_test.rb, and rewriting
|
|
215
|
+
// those belongs with the behaviour change rather than riding along with
|
|
216
|
+
// new primitives.
|
|
164
217
|
get missingInstalls() {
|
|
165
218
|
var self = this;
|
|
166
219
|
return this.installs.filter(function(i) {
|
|
@@ -195,9 +248,46 @@
|
|
|
195
248
|
this.connecting = false; this.picking = '';
|
|
196
249
|
}
|
|
197
250
|
} catch (e) {
|
|
198
|
-
|
|
251
|
+
// BOTH HALVES OR NEITHER. raw is what the WALLET said, captured
|
|
252
|
+
// before the mapper runs; msg is what the user reads. A mis-mapping
|
|
253
|
+
// is invisible in the mapped half alone, and that is not theoretical
|
|
254
|
+
// -- a correct mapper meeting a string it had never seen is how an
|
|
255
|
+
// empty Phantom came to be answered with balance advice seven times
|
|
256
|
+
// in one production session on 2026-09-06.
|
|
257
|
+
var raw = (e && e.message) || '';
|
|
258
|
+
var msg = (e && e.code === 4001) ? 'Signature rejected' : (raw || 'Connection failed');
|
|
199
259
|
if (typeof parseSolanaError === 'function') msg = parseSolanaError(msg);
|
|
200
260
|
this.error = msg; this.connecting = false; this.picking = '';
|
|
261
|
+
// ── OBSERVATION, AFTER THE USER HAS BEEN SERVED ──────────────────
|
|
262
|
+
//
|
|
263
|
+
// Deliberately the LAST statement in this block, and the three lines
|
|
264
|
+
// above are deliberately not in it: reporting may not delay, block or
|
|
265
|
+
// alter the line the user has just been handed.
|
|
266
|
+
//
|
|
267
|
+
// HOST-SUPPLIED, like every other global this partial reaches for
|
|
268
|
+
// (see CONTRACT WITH THE HOST'S JS in the header). The endpoint it
|
|
269
|
+
// posts to writes an ErrorLog, which is host-owned -- this gem ships
|
|
270
|
+
// no routes at all -- so a consumer that has not built one simply
|
|
271
|
+
// does not define this, the guard is false, and the card behaves
|
|
272
|
+
// exactly as it did before. Silence, never a 404.
|
|
273
|
+
//
|
|
274
|
+
// ALREADY REPORTED UPSTREAM? The connect helper substitutes its own
|
|
275
|
+
// sentence for an unusable wallet and reports the pair from in there,
|
|
276
|
+
// where the wallet's words still exist. Reporting again from out here
|
|
277
|
+
// would add a second row whose raw and mapped are BOTH our sentence
|
|
278
|
+
// -- the useless row, and the one an operator meets first.
|
|
279
|
+
//
|
|
280
|
+
// THE try/catch IS THIS GEM'S OWN LAYER, and it is not redundant with
|
|
281
|
+
// the reporter's. We do not own the reporter -- a host does, and a
|
|
282
|
+
// host's implementation may throw. A throw here escapes an async Alpine
|
|
283
|
+
// handler silently, which is the failure mode this whole feature
|
|
284
|
+
// exists to remove -- so the observation cannot be allowed to become
|
|
285
|
+
// the incident. Four values, and only these four: opts,
|
|
286
|
+
// verifyArgs() and this.props are all in scope and carry
|
|
287
|
+
// currentUserId, none of which belongs in an error log.
|
|
288
|
+
if (typeof window.reportWalletFailure === 'function' && !(e && e.walletFailureReported)) {
|
|
289
|
+
try { window.reportWalletFailure('wallet_connect', name, raw, msg); } catch (_e) {}
|
|
290
|
+
}
|
|
201
291
|
}
|
|
202
292
|
},
|
|
203
293
|
deepLink() {
|
|
@@ -64,6 +64,19 @@
|
|
|
64
64
|
you into that wallet's account, exactly as a standalone wallet button does. The
|
|
65
65
|
walletHint is shown precisely so that is a visible choice, not a surprise.
|
|
66
66
|
|
|
67
|
+
Two more host globals, both OPTIONAL and both reached behind a `typeof` guard:
|
|
68
|
+
`parseSolanaError(msg)` maps a wallet string to a sentence a user can act on,
|
|
69
|
+
and `window.reportWalletFailure(stage, provider, raw, mapped)` records a caught
|
|
70
|
+
rejection with stage 'web3_step_up'. The reporter is fire-and-forget — its
|
|
71
|
+
return value is ignored and a throw is swallowed — and it is called ONCE per
|
|
72
|
+
rejection, skipping any error solanaConnectAndVerify already reported upstream.
|
|
73
|
+
Send BOTH message halves: `raw` is what the wallet said, `mapped` is what the
|
|
74
|
+
user read, and only the pair makes a MIS-mapping diagnosable. A host that
|
|
75
|
+
defines neither gets this card exactly as it behaved before, with the failure
|
|
76
|
+
recorded nowhere. The reporter and its endpoint stay host-owned because they
|
|
77
|
+
need an ErrorLog this gem cannot assume — reference implementation in
|
|
78
|
+
turf-monster: app/javascript/solana_errors.js, POST /auth/solana/report_failure.
|
|
79
|
+
|
|
67
80
|
CRITICAL (Alpine): this partial is cloned from a <template x-if> by the modal
|
|
68
81
|
host, so it must have ONE root element, and the x-data below is a
|
|
69
82
|
DOUBLE-QUOTED attribute — a single " anywhere inside it (a code comment
|
|
@@ -167,9 +180,38 @@
|
|
|
167
180
|
}
|
|
168
181
|
this.error = (result && result.error) || 'Verification failed.';
|
|
169
182
|
} catch (e) {
|
|
170
|
-
|
|
183
|
+
// BOTH HALVES OR NEITHER. raw is what the WALLET said, captured
|
|
184
|
+
// before the mapper runs; msg is what the user reads. A mis-mapping
|
|
185
|
+
// is invisible in the mapped half alone -- a correct mapper meeting a
|
|
186
|
+
// string it had never seen is how an empty Phantom came to be
|
|
187
|
+
// answered with balance advice seven times in one production session
|
|
188
|
+
// on 2026-09-06, which is the incident this reporting exists for.
|
|
189
|
+
var raw = (e && e.message) || '';
|
|
190
|
+
var msg = (e && e.code === 4001) ? 'Signature rejected' : (raw || 'Connection failed');
|
|
171
191
|
if (typeof parseSolanaError === 'function') msg = parseSolanaError(msg);
|
|
172
192
|
this.error = msg;
|
|
193
|
+
// Observation, LAST and after the user has been served. Host-supplied
|
|
194
|
+
// like every other global here (see CONTRACT WITH THE HOST'S JS in
|
|
195
|
+
// the header): the endpoint it posts to writes an ErrorLog, which is
|
|
196
|
+
// host-owned -- this gem ships no routes -- so a consumer that built
|
|
197
|
+
// none simply leaves this undefined and the card behaves exactly as
|
|
198
|
+
// before. The walletFailureReported tag means solanaConnectAndVerify
|
|
199
|
+
// already reported this one from where the wallet's words still
|
|
200
|
+
// existed; reporting again would file a second row carrying OUR
|
|
201
|
+
// sentence in both halves, and that is the row an operator meets
|
|
202
|
+
// first. Sibling call site in modals/_wallet_connect carries the long
|
|
203
|
+
// form of both arguments.
|
|
204
|
+
//
|
|
205
|
+
// THE try/catch IS LOAD-BEARING HERE IN A WAY IT IS NOT THERE.
|
|
206
|
+
// this.connecting = false sits OUTSIDE this catch block, on the
|
|
207
|
+
// line below. A host reporter that throws would skip it, leaving the
|
|
208
|
+
// sign-in button disabled forever with an error the user cannot
|
|
209
|
+
// retry -- and Alpine swallows a throw out of an async handler, so
|
|
210
|
+
// nothing anywhere would say so. An observer that can wedge the card
|
|
211
|
+
// is a worse bug than the darkness it was added to fix.
|
|
212
|
+
if (typeof window.reportWalletFailure === 'function' && !(e && e.walletFailureReported)) {
|
|
213
|
+
try { window.reportWalletFailure('web3_step_up', name, raw, msg); } catch (_e) {}
|
|
214
|
+
}
|
|
173
215
|
}
|
|
174
216
|
this.connecting = false;
|
|
175
217
|
},
|
|
@@ -281,7 +323,7 @@
|
|
|
281
323
|
Installed badge, chevron — so a wallet reads identically everywhere it is
|
|
282
324
|
offered and this card does not invent a third look for one action.
|
|
283
325
|
|
|
284
|
-
It carries
|
|
326
|
+
It carries pulse-cta (engine-motion) because it is the ONE target on the
|
|
285
327
|
card and the whole point of the card is that the user should press it. %>
|
|
286
328
|
<template x-if="canOneClick">
|
|
287
329
|
<div>
|
data/lib/solana_studio/engine.rb
CHANGED
|
@@ -30,6 +30,10 @@ module SolanaStudio
|
|
|
30
30
|
|
|
31
31
|
app.config.assets.precompile += %w[
|
|
32
32
|
solana_studio/network_guard.js
|
|
33
|
+
solana_studio/wallet_transport.js
|
|
34
|
+
solana_studio/redirect_provider.js
|
|
35
|
+
solana_studio/wallet_journal.js
|
|
36
|
+
solana_studio/wallet_ops.js
|
|
33
37
|
]
|
|
34
38
|
end
|
|
35
39
|
end
|
|
@@ -16,5 +16,5 @@ module SolanaStudio
|
|
|
16
16
|
# through the normal cycle. Splitting the version out is the same shape
|
|
17
17
|
# studio-engine already uses (lib/studio/version.rb) and hands each file back
|
|
18
18
|
# to its real owner: this one to the release, the gemspec to the PR.
|
|
19
|
-
VERSION = "0.
|
|
19
|
+
VERSION = "0.8.0"
|
|
20
20
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solana-studio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ed25519
|
|
@@ -38,6 +38,10 @@ files:
|
|
|
38
38
|
- LICENSE
|
|
39
39
|
- README.md
|
|
40
40
|
- app/assets/javascripts/solana_studio/network_guard.js
|
|
41
|
+
- app/assets/javascripts/solana_studio/redirect_provider.js
|
|
42
|
+
- app/assets/javascripts/solana_studio/wallet_journal.js
|
|
43
|
+
- app/assets/javascripts/solana_studio/wallet_ops.js
|
|
44
|
+
- app/assets/javascripts/solana_studio/wallet_transport.js
|
|
41
45
|
- app/views/solana_studio/_deeplink_assets.html.erb
|
|
42
46
|
- app/views/solana_studio/_phantom_deeplink.html.erb
|
|
43
47
|
- app/views/solana_studio/auth/_wallet_credential.html.erb
|