Skip to content

Commit 96716fd

Browse files
committed
chore: use new sdk in aepp-wallet connection
1 parent eadb285 commit 96716fd

File tree

7 files changed

+294
-201
lines changed

7 files changed

+294
-201
lines changed

src/lib/sdkWallet.js

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import {
2+
MemoryAccount,
3+
AeSdkWallet,
4+
Node,
5+
BrowserWindowMessageConnection,
6+
WALLET_TYPE,
7+
RpcRejectedByUserError,
8+
RpcNoNetworkById,
9+
RpcNotAuthorizeError,
10+
encode,
11+
Encoding,
12+
SUBSCRIPTION_TYPES,
13+
} from '@aeternity/aepp-sdk-next';
14+
15+
function isRejectedByUserError(error) {
16+
return ['Rejected by user', 'Cancelled by user'].includes(error.message);
17+
}
18+
19+
class AccountStore extends MemoryAccount {
20+
#store;
21+
22+
constructor(address, store) {
23+
super(encode(Buffer.alloc(32), Encoding.AccountSecretKey));
24+
this.address = address;
25+
this.#store = store;
26+
}
27+
28+
async #switchAccount() {
29+
const initialAccountIdx = this.#store.state.accounts.activeIdx;
30+
const requiredAccountIdx = this.#store.state.accounts.list.findIndex(
31+
({ address }) => address === this.address,
32+
);
33+
await this.#store.dispatch('accounts/setActiveIdx', requiredAccountIdx);
34+
return () => this.#store.dispatch('accounts/setActiveIdx', initialAccountIdx);
35+
}
36+
37+
async sign(data, { signal } = {}) {
38+
const restore = await this.#switchAccount();
39+
try {
40+
return await this.#store.dispatch('accounts/sign', { data, signal });
41+
} catch (error) {
42+
if (isRejectedByUserError(error)) throw new RpcRejectedByUserError();
43+
throw error;
44+
} finally {
45+
await restore();
46+
}
47+
}
48+
49+
async signTransaction(transaction, { signal } = {}) {
50+
const restore = await this.#switchAccount();
51+
try {
52+
return await this.#store.dispatch('accounts/signTransaction', { transaction, signal });
53+
} catch (error) {
54+
if (isRejectedByUserError(error)) throw new RpcRejectedByUserError();
55+
throw error;
56+
} finally {
57+
await restore();
58+
}
59+
}
60+
}
61+
62+
function setupNodeWatch(store, sdk) {
63+
return store.watch(
64+
(_state, { node }) => node,
65+
(node) => {
66+
sdk.pool = new Map([['node', node]]);
67+
sdk.selectNode('node');
68+
},
69+
{ immediate: true },
70+
);
71+
}
72+
73+
function setupAccountsWatch(store, sdk, host, aeppId) {
74+
const getAccessibleAddresses = () =>
75+
store.getters.getApp(host)?.permissions.accessToAccounts ?? [];
76+
const getCurrentAddress = () => store.getters['accounts/active'].address;
77+
78+
const unwatchAppAddresses = store.watch(
79+
() => getAccessibleAddresses(),
80+
(addresses) => {
81+
sdk.accounts = Object.fromEntries(
82+
addresses.map((address) => [address, new AccountStore(address, store)]),
83+
);
84+
if (addresses.length) {
85+
const address = getCurrentAddress();
86+
sdk.selectAccount(sdk.accounts[address] ? address : addresses[0]);
87+
} else sdk._pushAccountsToApps();
88+
},
89+
{ immediate: true },
90+
);
91+
92+
let accountAccessPromise;
93+
function ensureCurrentAccountAccess() {
94+
async function ensureCurrentAccountAccessPure() {
95+
if (getAccessibleAddresses().includes(getCurrentAddress())) return;
96+
97+
const controller = new AbortController();
98+
const unsubscribe = store.watch(
99+
() => [getCurrentAddress(), getAccessibleAddresses()],
100+
([address, allowed]) => allowed.includes(address) && controller.abort(),
101+
);
102+
try {
103+
await store.dispatch('modals/open', {
104+
name: 'confirmAccountAccess',
105+
signal: controller.signal,
106+
appHost: host,
107+
});
108+
} catch (error) {
109+
if (error.message === 'Modal aborted') return;
110+
throw error;
111+
} finally {
112+
unsubscribe();
113+
}
114+
115+
const accountAddress = getCurrentAddress();
116+
if (!getAccessibleAddresses().includes(accountAddress)) {
117+
store.commit('toggleAccessToAccount', { appHost: host, accountAddress });
118+
}
119+
}
120+
121+
accountAccessPromise ??= ensureCurrentAccountAccessPure().finally(() => {
122+
accountAccessPromise = null;
123+
});
124+
return accountAccessPromise;
125+
}
126+
127+
const unwatchCurrentAddress = store.watch(
128+
() => getCurrentAddress(),
129+
async (address) => {
130+
if (getAccessibleAddresses().includes(address)) {
131+
sdk.selectAccount(address);
132+
return;
133+
}
134+
const client = sdk._getClient(aeppId);
135+
if (client.addressSubscription.size === 0) return;
136+
ensureCurrentAccountAccess();
137+
},
138+
{ immediate: true },
139+
);
140+
141+
return [
142+
ensureCurrentAccountAccess,
143+
() => [unwatchAppAddresses, unwatchCurrentAddress].forEach((unwatch) => unwatch()),
144+
];
145+
}
146+
147+
function setupConnection(target, sdk) {
148+
const connection = new BrowserWindowMessageConnection({ target });
149+
const aeppId = sdk.addRpcClient(connection);
150+
sdk.shareWalletInfo(aeppId);
151+
const intervalId = setInterval(() => sdk.shareWalletInfo(aeppId), 3000);
152+
return [
153+
aeppId,
154+
() => {
155+
clearInterval(intervalId);
156+
if (sdk._clients.has(aeppId)) sdk.removeRpcClient(aeppId);
157+
},
158+
];
159+
}
160+
161+
export default (store, target, host) => {
162+
let aeppInfo;
163+
let authAeppId;
164+
let ensureCurrentAccountAccess;
165+
let unbindConnection;
166+
let unbindAccounts;
167+
168+
function ensureAuthorized(aeppId, origin) {
169+
const originHost = new URL(origin).host;
170+
host ??= originHost;
171+
if (originHost === host && aeppId === authAeppId) return;
172+
throw new RpcNotAuthorizeError();
173+
}
174+
175+
function confirmAction(action) {
176+
const res = confirm(`Aepp "${aeppInfo.name}" at ${origin} is requesting ${action}`);
177+
if (res === false) throw new RpcRejectedByUserError();
178+
}
179+
180+
const sdk = new AeSdkWallet({
181+
id: window.origin,
182+
type: WALLET_TYPE.window,
183+
name: 'Base Aepp',
184+
onConnection: (aeppId, params, origin) => {
185+
ensureAuthorized(aeppId, origin);
186+
aeppInfo = params;
187+
[ensureCurrentAccountAccess, unbindAccounts] = setupAccountsWatch(store, sdk, host, aeppId);
188+
},
189+
onSubscription: (aeppId, params, origin) => {
190+
ensureAuthorized(aeppId, origin);
191+
if (params.type === SUBSCRIPTION_TYPES.subscribe) void ensureCurrentAccountAccess();
192+
},
193+
onAskAccounts: async (aeppId, _params, origin) => {
194+
ensureAuthorized(aeppId, origin);
195+
try {
196+
await ensureCurrentAccountAccess();
197+
} catch (error) {
198+
if (isRejectedByUserError(error)) throw new RpcRejectedByUserError();
199+
throw error;
200+
}
201+
},
202+
onAskToSelectNetwork: async (aeppId, parameters, origin) => {
203+
ensureAuthorized(aeppId, origin);
204+
205+
function switchToNetwork({ name, url, networkId }) {
206+
const details = [url, networkId].filter(Boolean).join(', ');
207+
confirmAction(`a network switch to ${name} (${details})`);
208+
store.commit('setSdkUrl', url);
209+
}
210+
211+
if (parameters.networkId) {
212+
const network = (
213+
await Promise.allSettled(
214+
store.getters.networks.map(async (network) => ({
215+
...network,
216+
networkId: await new Node(network.url).getNetworkId(),
217+
})),
218+
)
219+
)
220+
.filter(({ status }) => status === 'fulfilled')
221+
.map(({ value }) => value)
222+
.find(({ networkId }) => networkId === parameters.networkId);
223+
if (network == null) throw new RpcNoNetworkById(parameters.networkId);
224+
switchToNetwork(network);
225+
return;
226+
}
227+
228+
const network = store.getters.networks.find(
229+
({ url }) => new URL(url).toString() === new URL(parameters.nodeUrl).toString(),
230+
);
231+
if (network) {
232+
switchToNetwork(network);
233+
return;
234+
}
235+
236+
const networkId = await new Node(parameters.nodeUrl).getNetworkId();
237+
confirmAction(`a network switch to ${parameters.nodeUrl} (${networkId})`);
238+
store.commit('addNetwork', {
239+
name: `By ${host}`,
240+
url: parameters.nodeUrl,
241+
});
242+
store.commit('setSdkUrl', parameters.nodeUrl);
243+
},
244+
onDisconnect: (aeppId, params) => {
245+
unbindConnection();
246+
unbindAccounts();
247+
[authAeppId, unbindConnection] = setupConnection(target, sdk);
248+
},
249+
});
250+
251+
const unbindNodeWatch = setupNodeWatch(store, sdk);
252+
253+
[authAeppId, unbindConnection] = setupConnection(target, sdk);
254+
255+
return () => {
256+
unbindConnection();
257+
unbindAccounts?.();
258+
unbindNodeWatch();
259+
};
260+
};

src/pages/aens/NameDetails.vue

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,14 @@ export default {
9999
const requiredAccountIdx = this.$store.state.accounts.list.findIndex(
100100
({ address }) => address === this.details.owner,
101101
);
102-
if (initialAccountIdx !== requiredAccountIdx) {
103-
this.$store.commit('accounts/setActiveIdx', requiredAccountIdx);
104-
}
105-
await this.$store.dispatch('names/updatePointer', { name: this.name, address: this.address });
106-
if (initialAccountIdx !== requiredAccountIdx) {
107-
this.$store.commit('accounts/setActiveIdx', initialAccountIdx);
102+
await this.$store.dispatch('accounts/setActiveIdx', requiredAccountIdx);
103+
try {
104+
await this.$store.dispatch('names/updatePointer', {
105+
name: this.name,
106+
address: this.address,
107+
});
108+
} finally {
109+
await this.$store.dispatch('accounts/setActiveIdx', initialAccountIdx);
108110
}
109111
},
110112
async goToTransactionDetails() {

src/pages/mobile/AppBrowser.vue

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141

4242
<script>
4343
import { mapState } from 'vuex';
44-
import BrowserWindowMessageConnection from '@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/connection/browser-window-message';
44+
import sdkWallet from '../../lib/sdkWallet';
4545
import { PROTOCOLS_ALLOWED, PROTOCOL_DEFAULT } from '../../lib/constants';
4646
import UrlForm from '../../components/mobile/UrlForm.vue';
4747
import ButtonPlain from '../../components/ButtonPlain.vue';
@@ -92,23 +92,14 @@ export default {
9292
}),
9393
},
9494
async mounted() {
95-
const sdk = await this.$store.state.sdk;
96-
97-
const connection = BrowserWindowMessageConnection({ target: this.$refs.iframe.contentWindow });
98-
sdk.addRpcClient(connection);
99-
const shareWalletInfoInterval = setInterval(
100-
() => sdk.shareWalletInfo(connection.sendMessage.bind(connection)),
101-
3000,
102-
);
103-
95+
const unbind = sdkWallet(this.$store, this.$refs.iframe.contentWindow, this.host);
10496
const handler = () => {
10597
this.showMenu = false;
10698
};
10799
window.addEventListener('blur', handler);
108100
this.$once('hook:destroyed', () => {
109101
window.removeEventListener('blur', handler);
110-
clearInterval(shareWalletInfoInterval);
111-
Object.keys(sdk.rpcClients).forEach((id) => sdk.removeRpcClient(id));
102+
unbind();
112103
});
113104
},
114105
methods: {

src/store/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import accountsModule from './modules/accounts';
1111
import runMigrations from './migrations';
1212
import persistState from './plugins/persistState';
1313
import remoteConnection from './plugins/remoteConnection';
14-
import initSdk from './plugins/initSdk';
1514
import sdk from './plugins/sdk';
1615
import registerServiceWorker from './plugins/registerServiceWorker';
1716
import reverseIframe from './plugins/reverseIframe';
@@ -80,7 +79,6 @@ export default new Vuex.Store({
8079
}),
8180
}),
8281
),
83-
initSdk,
8482
sdk,
8583
...(RUNNING_IN_POPUP
8684
? []

src/store/modules/accounts/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export default {
6767
},
6868

6969
actions: {
70+
setActiveIdx({ commit, state }, activeIdx) {
71+
if (state.activeIdx === activeIdx) return;
72+
commit('setActiveIdx', activeIdx);
73+
},
74+
7075
sign({ getters: { active, getModule }, dispatch }, { data, signal }) {
7176
return dispatch(`${getModule(active).name}/sign`, { data, signal });
7277
},

0 commit comments

Comments
 (0)