solana-studio 0.8.0 → 0.9.1
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a00c3f09eb98073668c9218045e49e705be6e803348ad9338e4295d32ca7c091
|
|
4
|
+
data.tar.gz: 16189d3a834e1880bbba0b37113823fdf95d4a690926d98f67120213818b70db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43e0a031a5fb9c2bcb451c4bddcf2df0e79a1c1722c60e57e9fcd302699434736bf363dd12a6f1ee7f847f447a1b3aeb792e557a7874edc419714a66a9a8b094
|
|
7
|
+
data.tar.gz: 8613a4f8bcead4253fb9b1814b1fbb008d9d1f9c3db4e1e8be33b5ca5fc005ce2b7b4226f136454a52f4ef66ccec59c22cf968d6f78762558e51355a2e2de056
|
|
@@ -11,11 +11,22 @@
|
|
|
11
11
|
//
|
|
12
12
|
// walletOps.define('contest_entry', {
|
|
13
13
|
// prepare: function (ctx) { ... return { transaction: <base58>, ...state }; },
|
|
14
|
-
// complete: function (ctx, result, state) { ... }
|
|
14
|
+
// complete: function (ctx, result, state) { ... },
|
|
15
|
+
// signOnly: true
|
|
15
16
|
// });
|
|
16
17
|
//
|
|
17
18
|
// walletOps.run('contest_entry', { contestId: 12 }, { provider: ... });
|
|
18
19
|
//
|
|
20
|
+
// `signOnly` IS A REQUIREMENT OF THE TRANSACTION, NOT A PREFERENCE ABOUT THE
|
|
21
|
+
// WALLET, and it is the intent's to declare because only the intent knows the
|
|
22
|
+
// shape of the bytes it prepared. A CO-SIGNED transaction — one whose second
|
|
23
|
+
// signer slot is deliberately empty because a server fills it — cannot be
|
|
24
|
+
// broadcast by the wallet: the chain would reject it for a missing required
|
|
25
|
+
// signature, and, worse, the signed bytes the server needs would never come
|
|
26
|
+
// back to the app, so the flow fails with nothing to retry. Declaring it makes
|
|
27
|
+
// the transaction's own requirement outrank the wallet's capability. Omit it
|
|
28
|
+
// and nothing changes: a wallet that can broadcast still does.
|
|
29
|
+
//
|
|
19
30
|
// THE ONE RULE A CALLER MUST FOLLOW: handlers are registered BY NAME at page
|
|
20
31
|
// load, not passed as closures. A closure is precisely what cannot survive the
|
|
21
32
|
// redirect — the page that held it no longer exists when the wallet answers. The
|
|
@@ -30,7 +41,8 @@
|
|
|
30
41
|
// WHAT THIS FILE DOES NOT DO: it does not broadcast. Signing and sending are
|
|
31
42
|
// different responsibilities with different failure modes, and the wallet that
|
|
32
43
|
// broadcasts differs per vendor (Phantom deprecated its send-side deeplink, so
|
|
33
|
-
// the app sends; Solflare and Backpack send for you)
|
|
44
|
+
// the app sends; Solflare and Backpack send for you) — and, where the intent
|
|
45
|
+
// declares `signOnly`, per TRANSACTION as well. `complete` is told which
|
|
34
46
|
// happened and owns the RPC, exactly as the existing flows already do.
|
|
35
47
|
(function (W) {
|
|
36
48
|
'use strict';
|
|
@@ -107,6 +119,23 @@
|
|
|
107
119
|
return Promise.resolve(handler.prepare(ctx)).then(function (prepared) {
|
|
108
120
|
var intent = { op: name, ctx: ctx, state: prepared };
|
|
109
121
|
|
|
122
|
+
// THE DECLARATION TRAVELS IN THE JOURNAL, NOT LOOKED UP FROM THE HANDLER
|
|
123
|
+
// AT THE FAR END, and that is the whole reason this line exists here
|
|
124
|
+
// rather than inside signingHop. The signing hop is taken from TWO
|
|
125
|
+
// places: this one, where the handler is certainly registered, and the
|
|
126
|
+
// connect callback, which is a DIFFERENT PAGE that may not have loaded
|
|
127
|
+
// the script that defined this intent. A handler lookup there would come
|
|
128
|
+
// back empty and silently fall through to the send-side default — the
|
|
129
|
+
// exact branch a co-signed transaction must never take. A boolean on the
|
|
130
|
+
// intent is JSON-serialisable, which is the one thing this file requires
|
|
131
|
+
// of anything that has to survive a redirect.
|
|
132
|
+
//
|
|
133
|
+
// Set ONLY when true, so the journal an undeclared intent writes is
|
|
134
|
+
// byte-identical to the one it wrote before this option existed. That is
|
|
135
|
+
// also why JOURNAL_VERSION does not move: no reader's expectations
|
|
136
|
+
// change, and bumping it would strand every trip already in flight.
|
|
137
|
+
if (handler.signOnly) intent.signOnly = true;
|
|
138
|
+
|
|
110
139
|
// Already connected? Go straight to signing. Otherwise connect first and
|
|
111
140
|
// carry the intent through — sessions do not expire on any of the three
|
|
112
141
|
// wallets, so this branch is taken once per user, not once per action.
|
|
@@ -145,18 +174,38 @@
|
|
|
145
174
|
return signingHop(provider, connected, opts);
|
|
146
175
|
}
|
|
147
176
|
|
|
148
|
-
// Which signing method this wallet gets is
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
177
|
+
// Which signing method this wallet gets is TWO questions asked in order, and
|
|
178
|
+
// the order is the point.
|
|
179
|
+
//
|
|
180
|
+
// FIRST, what does the TRANSACTION allow? A co-signed transaction has an
|
|
181
|
+
// empty signer slot the server fills, so the wallet must sign and hand the
|
|
182
|
+
// bytes back — broadcasting it is not a worse option, it is a broken one.
|
|
183
|
+
// An intent that knows this declares `signOnly` and that answer is final.
|
|
184
|
+
//
|
|
185
|
+
// SECOND, and only for everything else, what can the WALLET do? Phantom's
|
|
186
|
+
// send-side deeplink is deprecated, so it signs and the app broadcasts;
|
|
187
|
+
// Solflare and Backpack broadcast for you. Asking the provider keeps that
|
|
188
|
+
// fact in the profile table where it is asserted, rather than branching on a
|
|
189
|
+
// wallet name here.
|
|
190
|
+
//
|
|
191
|
+
// Asking them the other way round is the bug this replaced: capability alone
|
|
192
|
+
// sent every co-signed transaction to the wallet's broadcaster on any wallet
|
|
193
|
+
// that had one.
|
|
194
|
+
//
|
|
195
|
+
// A wallet with no signTransaction at all would be refused BY NAME by the
|
|
196
|
+
// provider's own capability gate rather than quietly broadcast here. No
|
|
197
|
+
// profile is in that state today (all three sign), and this is the branch
|
|
198
|
+
// that would have to be revisited if one ever were.
|
|
152
199
|
function signingHop(provider, journal, opts) {
|
|
200
|
+
var intent = journal.intent;
|
|
153
201
|
var payload = {
|
|
154
202
|
journal: journal,
|
|
155
|
-
transaction:
|
|
203
|
+
transaction: intent.state.transaction,
|
|
156
204
|
redirectLink: opts.redirectLink,
|
|
157
|
-
intent:
|
|
205
|
+
intent: intent
|
|
158
206
|
};
|
|
159
|
-
|
|
207
|
+
var signOnly = !!(intent && intent.signOnly);
|
|
208
|
+
return (!signOnly && provider.can('signAndSendTransaction'))
|
|
160
209
|
? provider.beginSignAndSendTransaction(payload)
|
|
161
210
|
: provider.beginSignTransaction(payload);
|
|
162
211
|
}
|
|
@@ -234,6 +283,18 @@
|
|
|
234
283
|
if (!handler || typeof handler.prepare !== 'function' || typeof handler.complete !== 'function') {
|
|
235
284
|
throw new Error('walletOps.define("' + name + '") needs both prepare and complete');
|
|
236
285
|
}
|
|
286
|
+
// REFUSED RATHER THAN COERCED, because every wrong answer here fails in
|
|
287
|
+
// the dangerous direction. A typo'd `signOnly: 'true'` read as truthy
|
|
288
|
+
// would sign-only a flow that wanted the wallet to broadcast; read
|
|
289
|
+
// strictly, it silently sends a co-signed transaction to a broadcaster.
|
|
290
|
+
// Neither is discoverable at the call site, and both surface as a chain
|
|
291
|
+
// error one page death later. So the option is a boolean or it is a bug.
|
|
292
|
+
if (handler.signOnly !== undefined && typeof handler.signOnly !== 'boolean') {
|
|
293
|
+
throw new Error(
|
|
294
|
+
'walletOps.define("' + name + '") signOnly must be true or false, got ' +
|
|
295
|
+
typeof handler.signOnly
|
|
296
|
+
);
|
|
297
|
+
}
|
|
237
298
|
handlers[name] = handler;
|
|
238
299
|
},
|
|
239
300
|
|
|
@@ -214,11 +214,55 @@
|
|
|
214
214
|
// assertions in test/views/wallet_connect_picker_test.rb, and rewriting
|
|
215
215
|
// those belongs with the behaviour change rather than riding along with
|
|
216
216
|
// new primitives.
|
|
217
|
+
// THE REDIRECT-TRANSPORT REGISTRY, guarded the same way canDeepLink is and
|
|
218
|
+
// for the same reason: a consumer that does not load
|
|
219
|
+
// solana_studio/redirect_provider.js has no registry, and an absent
|
|
220
|
+
// capability must not default to the permissive branch. Empty list, no
|
|
221
|
+
// rows, install rows keep their place.
|
|
222
|
+
get redirectProviders() {
|
|
223
|
+
var rp = window.SolanaStudio && window.SolanaStudio.redirectProvider;
|
|
224
|
+
if (!rp || typeof rp.all !== 'function') return [];
|
|
225
|
+
return rp.all().filter(function(w) { return w.can('browse'); });
|
|
226
|
+
},
|
|
227
|
+
// Wallets we can hand a phone to RIGHT NOW, and the honesty rule that
|
|
228
|
+
// shapes this list: a row is painted only when tapping it does something.
|
|
229
|
+
//
|
|
230
|
+
// The handoff is the browse deeplink — it opens this page inside the
|
|
231
|
+
// wallet's OWN in-app browser, where the wallet injects a provider and the
|
|
232
|
+
// ordinary detected-row flow above works unchanged. That needs no signing
|
|
233
|
+
// protocol on our side, which is exactly why it can ship before one.
|
|
234
|
+
//
|
|
235
|
+
// Excluded: a wallet already injected (the detected row above is a better
|
|
236
|
+
// offer than leaving the page), and Phantom when canDeepLink is true
|
|
237
|
+
// (its own row below is a full sign-in, not a handoff — offering both
|
|
238
|
+
// would be two Phantom rows racing each other).
|
|
239
|
+
get mobileHandoffs() {
|
|
240
|
+
if (!this.isMobile) return [];
|
|
241
|
+
var self = this;
|
|
242
|
+
return this.redirectProviders.filter(function(w) {
|
|
243
|
+
if (self.hasWallet(w.name)) return false;
|
|
244
|
+
if (w.key === 'phantom' && self.canDeepLink) return false;
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
// Navigate into the wallet's browser, carrying THIS page as the target so
|
|
249
|
+
// the user lands where they already were rather than on a home screen.
|
|
250
|
+
openInWallet(key) {
|
|
251
|
+
var rp = window.SolanaStudio && window.SolanaStudio.redirectProvider;
|
|
252
|
+
var w = rp && rp.forWallet(key);
|
|
253
|
+
if (!w) return;
|
|
254
|
+
window.location.href = w.browseUrl(window.location.href, window.location.origin);
|
|
255
|
+
},
|
|
217
256
|
get missingInstalls() {
|
|
218
257
|
var self = this;
|
|
258
|
+
var handoffs = this.mobileHandoffs.map(function(w) { return w.name.toLowerCase(); });
|
|
219
259
|
return this.installs.filter(function(i) {
|
|
220
260
|
if (self.hasWallet(i.name)) return false;
|
|
221
261
|
if (self.isMobile && self.canDeepLink && i.name === 'Phantom') return false;
|
|
262
|
+
// A wallet with a working handoff row must not ALSO offer a desktop
|
|
263
|
+
// download page. That pairing is what sent Solflare and Backpack users
|
|
264
|
+
// on a phone to solflare.com/download — a dead end with no error.
|
|
265
|
+
if (self.isMobile && handoffs.indexOf(i.name.toLowerCase()) !== -1) return false;
|
|
222
266
|
return true;
|
|
223
267
|
});
|
|
224
268
|
},
|
|
@@ -387,6 +431,23 @@
|
|
|
387
431
|
</span>
|
|
388
432
|
</button>
|
|
389
433
|
|
|
434
|
+
<%# Wallets we can hand this phone to. Rendered ABOVE the install rows on
|
|
435
|
+
purpose: a path that works belongs before a download page. Each row is
|
|
436
|
+
painted only when tapping it does something — see mobileHandoffs. %>
|
|
437
|
+
<template x-for="w in mobileHandoffs" :key="'handoff-' + w.key">
|
|
438
|
+
<button type="button" @click="openInWallet(w.key)"
|
|
439
|
+
class="w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition text-left">
|
|
440
|
+
<svg class="w-9 h-9 shrink-0" aria-hidden="true"><use :href="'#se-wallet-' + brandIcon(w.name)"></use></svg>
|
|
441
|
+
<span class="font-semibold text-heading" x-text="w.name"></span>
|
|
442
|
+
<span class="ml-auto flex items-center gap-2">
|
|
443
|
+
<span class="text-xs text-muted uppercase tracking-wide">Open app</span>
|
|
444
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-muted">
|
|
445
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
446
|
+
</svg>
|
|
447
|
+
</span>
|
|
448
|
+
</button>
|
|
449
|
+
</template>
|
|
450
|
+
|
|
390
451
|
<%# Featured wallets that aren't installed — open the install page %>
|
|
391
452
|
<template x-for="i in missingInstalls" :key="i.name">
|
|
392
453
|
<a :href="i.url" target="_blank" rel="noopener noreferrer"
|
|
@@ -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.9.1"
|
|
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.9.1
|
|
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-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ed25519
|